From 8ccc5f9b8e7e64a0820dea63e5f08e8707d58c1e Mon Sep 17 00:00:00 2001 From: Yicong Huang <17627829+Yicong-Huang@users.noreply.github.com> Date: Thu, 17 Oct 2024 00:43:56 -0700 Subject: [PATCH 01/33] initial wip --- .../handlers/control/debug_command_handler.py | 2 + .../control/replay_current_tuple_handler.py | 1 + .../architecture/managers/pause_manager.py | 1 + .../ConsoleMessageHandler.scala | 5 - core/gui/custom-webpack.config.js | 13 +- core/gui/package.json | 1 + .../code-editor.component.html | 1 + .../code-editor.component.scss | 61 ++++ .../code-editor.component.ts | 321 +++++++++++++++++- .../component/menu/menu.component.ts | 8 +- .../execute-workflow.service.ts | 4 +- .../operator-debug/udf-debug.service.ts | 273 +++++++++++++++ .../workflow-graph/model/shared-model.ts | 2 + .../model/workflow-action.service.ts | 2 +- .../workflow-websocket.service.ts | 86 +++++ .../types/workflow-websocket.interface.ts | 15 +- core/gui/yarn.lock | 5 + 17 files changed, 775 insertions(+), 26 deletions(-) create mode 100644 core/gui/src/app/workspace/service/operator-debug/udf-debug.service.ts diff --git a/core/amber/src/main/python/core/architecture/handlers/control/debug_command_handler.py b/core/amber/src/main/python/core/architecture/handlers/control/debug_command_handler.py index 7a31f1a3b02..b412c08cdd8 100644 --- a/core/amber/src/main/python/core/architecture/handlers/control/debug_command_handler.py +++ b/core/amber/src/main/python/core/architecture/handlers/control/debug_command_handler.py @@ -17,6 +17,8 @@ def __call__(self, context: Context, command: cmd, *args, **kwargs): context.debug_manager.put_debug_command(translated_command) # allow MainLoop to switch into DataProcessor. + context.pause_manager.resume(PauseType.USER_PAUSE) + context.pause_manager.resume(PauseType.EXCEPTION_PAUSE) context.pause_manager.resume(PauseType.DEBUG_PAUSE) @staticmethod diff --git a/core/amber/src/main/python/core/architecture/handlers/control/replay_current_tuple_handler.py b/core/amber/src/main/python/core/architecture/handlers/control/replay_current_tuple_handler.py index 8efd04e9ee7..6543c8cb895 100644 --- a/core/amber/src/main/python/core/architecture/handlers/control/replay_current_tuple_handler.py +++ b/core/amber/src/main/python/core/architecture/handlers/control/replay_current_tuple_handler.py @@ -20,5 +20,6 @@ def __call__(self, context: Context, command: cmd, *args, **kwargs): [context.tuple_processing_manager.current_input_tuple], context.tuple_processing_manager.current_input_tuple_iter, ) + context.pause_manager.resume(PauseType.USER_PAUSE) context.pause_manager.resume(PauseType.EXCEPTION_PAUSE) return None diff --git a/core/amber/src/main/python/core/architecture/managers/pause_manager.py b/core/amber/src/main/python/core/architecture/managers/pause_manager.py index 307640f8b7d..18d15694c14 100644 --- a/core/amber/src/main/python/core/architecture/managers/pause_manager.py +++ b/core/amber/src/main/python/core/architecture/managers/pause_manager.py @@ -56,6 +56,7 @@ def resume(self, pause_type: PauseType, change_state=True) -> None: logger.debug("resume by " + str(pause_type)) if pause_type in self._global_pauses: self._global_pauses.remove(pause_type) + self._global_pauses.clear() # del self._specific_input_pauses[pause_type] # still globally paused no action, don't need to resume anything diff --git a/core/amber/src/main/scala/edu/uci/ics/amber/engine/architecture/controller/promisehandlers/ConsoleMessageHandler.scala b/core/amber/src/main/scala/edu/uci/ics/amber/engine/architecture/controller/promisehandlers/ConsoleMessageHandler.scala index e1a6760aebc..6216b813bff 100644 --- a/core/amber/src/main/scala/edu/uci/ics/amber/engine/architecture/controller/promisehandlers/ConsoleMessageHandler.scala +++ b/core/amber/src/main/scala/edu/uci/ics/amber/engine/architecture/controller/promisehandlers/ConsoleMessageHandler.scala @@ -15,11 +15,6 @@ trait ConsoleMessageHandler { this: ControllerAsyncRPCHandlerInitializer => registerHandler[ConsoleMessageTriggered, Unit] { (msg, sender) => { - if (msg.consoleMessage.msgType.isError) { - // if its an error message, pause the workflow - execute(PauseWorkflow(), CONTROLLER) - } - // forward message to frontend sendToClient(msg) } diff --git a/core/gui/custom-webpack.config.js b/core/gui/custom-webpack.config.js index ece14dbfda0..cdf3466c185 100644 --- a/core/gui/custom-webpack.config.js +++ b/core/gui/custom-webpack.config.js @@ -4,13 +4,16 @@ module.exports = { { test: /\.css$/, use: ["style-loader", "css-loader"], - include: [require("path").resolve(__dirname, "node_modules/monaco-editor")], + include: [ + require("path").resolve(__dirname, "node_modules/monaco-editor"), + require("path").resolve(__dirname, "node_modules/monaco-breakpoints") + ], }, ], parser: { javascript: { - url: true - } - } + url: true, + }, + }, }, -}; \ No newline at end of file +}; diff --git a/core/gui/package.json b/core/gui/package.json index a6bdd81ccbf..4b89fd97c66 100644 --- a/core/gui/package.json +++ b/core/gui/package.json @@ -62,6 +62,7 @@ "@codingame/monaco-vscode-java-default-extension": "8.0.4", "vscode": "npm:@codingame/monaco-vscode-api@8.0.4", "monaco-editor": "npm:@codingame/monaco-vscode-editor-api@8.0.4", + "monaco-breakpoints": "0.1.2", "ng-zorro-antd": "16.2.2", "ng2-pdf-viewer": "9.1.5", "ngx-color-picker": "12.0.1", diff --git a/core/gui/src/app/workspace/component/code-editor-dialog/code-editor.component.html b/core/gui/src/app/workspace/component/code-editor-dialog/code-editor.component.html index 63f615a31ac..9fc88b56301 100644 --- a/core/gui/src/app/workspace/component/code-editor-dialog/code-editor.component.html +++ b/core/gui/src/app/workspace/component/code-editor-dialog/code-editor.component.html @@ -4,6 +4,7 @@ class="box" cdkDrag cdkDragBoundary="body" + (mouseleave)="onMouseLeave()" (focusin)="onFocus()">
= new Subject(); private currentOperatorId!: string; + private breakpointManager: BreakpointManager | undefined; public title: string | undefined; public formControl!: FormControl; public componentRef: ComponentRef | undefined; @@ -71,6 +100,10 @@ export class CodeEditorComponent implements AfterViewInit, SafeStyle, OnDestroy private userResponseSubject?: Subject; private isMultipleVariables: boolean = false; + public isUpdatingBreakpoints = false; + public instance: MonacoBreakpoint | undefined = undefined; + public lastBreakLine = 0; + private generateLanguageTitle(language: string): string { return `${language.charAt(0).toUpperCase()}${language.slice(1)} UDF`; } @@ -85,7 +118,11 @@ export class CodeEditorComponent implements AfterViewInit, SafeStyle, OnDestroy private workflowActionService: WorkflowActionService, private workflowVersionService: WorkflowVersionService, public coeditorPresenceService: CoeditorPresenceService, - private aiAssistantService: AIAssistantService + private aiAssistantService: AIAssistantService, + public executeWorkflowService: ExecuteWorkflowService, + public workflowWebsocketService: WorkflowWebsocketService, + public udfDebugService: UdfDebugService, + private renderer: Renderer2, ) { this.currentOperatorId = this.workflowActionService.getJointGraphWrapper().getCurrentHighlightedOperatorIDs()[0]; const operatorType = this.workflowActionService.getTexeraGraph().getOperator(this.currentOperatorId).operatorType; @@ -115,6 +152,7 @@ export class CodeEditorComponent implements AfterViewInit, SafeStyle, OnDestroy // hacky solution to reset view after view is rendered. const style = localStorage.getItem(this.currentOperatorId); if (style) this.containerElement.nativeElement.style.cssText = style; + this.breakpointManager = this.udfDebugService.getOrCreateManager(this.currentOperatorId); // start editor this.workflowVersionService @@ -127,6 +165,120 @@ export class CodeEditorComponent implements AfterViewInit, SafeStyle, OnDestroy this.initializeMonacoEditor(); } }); + + this.workflowWebsocketService + .subscribeToEvent("ConsoleUpdateEvent") + .pipe(untilDestroyed(this)) + .subscribe((pythonConsoleUpdateEvent: ConsoleUpdateEvent) => { + const operatorId = pythonConsoleUpdateEvent.operatorId; + pythonConsoleUpdateEvent.messages + .filter(consoleMessage => consoleMessage.msgType.name === "DEBUGGER") + .map( + consoleMessage => { + console.log(consoleMessage); + const pattern = /^Breakpoint (\d+).*\.py:(\d+)\s*$/; + return consoleMessage.title.match(pattern); + }, + ) + .filter(isDefined) + .forEach(match => { + const breakpoint = Number(match[1]); + const lineNumber = Number(match[2]); + this.breakpointManager?.assignBreakpointId(lineNumber, breakpoint); + }); + + if (pythonConsoleUpdateEvent.messages.length === 0) { + return; + } + let lastMsg = pythonConsoleUpdateEvent.messages[pythonConsoleUpdateEvent.messages.length - 1]; + if (lastMsg.title.startsWith("break")) { + this.lastBreakLine = Number(lastMsg.title.split(" ")[1]); + } + if (lastMsg.title.startsWith("*** Blank or comment")) { + this.isUpdatingBreakpoints = true; + this.instance!["lineNumberAndDecorationIdMap"].forEach((v: string, k: number, m: Map) => { + if (k === this.lastBreakLine) { + console.log("removing " + k); + this.breakpointManager?.removeBreakpoint(k); + this.instance!["removeSpecifyDecoration"](v, k); + } + }); + this.isUpdatingBreakpoints = false; + } + }); + } + + private getMouseEventTarget(e: EditorMouseEvent) { + return { ...(e.target as EditorMouseTarget) }; + } + + showTooltip(mouseX: number, mouseY: number, lineNum: number, breakpointManager: BreakpointManager): void { + // Create tooltip element + const tooltip = this.renderer.createElement("div"); + this.renderer.addClass(tooltip, "custom-tooltip"); + + // Create header element + const header = this.renderer.createElement("div"); + this.renderer.addClass(header, "tooltip-header"); + this.renderer.setProperty(header, "innerText", `Condition on line ${lineNum}:`); + + // Create textarea element + const textarea = this.renderer.createElement("textarea"); + this.renderer.addClass(textarea, "custom-textarea"); + let oldCondition = breakpointManager.getCondition(lineNum); + this.renderer.setProperty(textarea, "value", oldCondition ?? ""); + + // Append header and textarea to tooltip + this.renderer.appendChild(tooltip, header); + this.renderer.appendChild(tooltip, textarea); + + // Append tooltip to the document body + this.renderer.appendChild(document.body, tooltip); + textarea.focus(); + // Function to remove the tooltip + const removeTooltip = () => { + const inputValue = textarea.value; + if (inputValue != oldCondition) { + breakpointManager.setCondition(lineNum, inputValue); + } + if (removeTooltipListener) { + removeTooltipListener(); + } + if (removeFocusoutListener) { + removeFocusoutListener(); + } + // Add fade-out class + this.renderer.addClass(tooltip, "fade-out"); + // Remove tooltip after the transition ends + const transitionEndListener = this.renderer.listen(tooltip, "transitionend", () => { + tooltip.remove(); + transitionEndListener(); + }); + }; + + // Listen for Enter key press to exit edit mode + const removeTooltipListener = this.renderer.listen(textarea, "keydown", (event: KeyboardEvent) => { + if (event.key === "Enter" && !event.shiftKey) { + event.preventDefault(); + removeTooltip(); // Trigger fade-out and remove the tooltip + } + }); + + // Listen for focusout event to remove the tooltip after 1 second + const removeFocusoutListener = this.renderer.listen(textarea, "focusout", () => { + setTimeout(removeTooltip, 300); + }); + + // Calculate tooltip dimensions after appending to the DOM + const tooltipRect = tooltip.getBoundingClientRect(); + + // Adjust the position to appear at the left side of the mouse + const adjustedX = mouseX - tooltipRect.width - 10; // Subtracting width and adding some offset to the left + const adjustedY = mouseY - tooltipRect.height / 2; + + // Update tooltip position + this.renderer.setStyle(tooltip, "top", `${adjustedY}px`); + this.renderer.setStyle(tooltip, "left", `${adjustedX}px`); } ngOnDestroy(): void { @@ -145,6 +297,24 @@ export class CodeEditorComponent implements AfterViewInit, SafeStyle, OnDestroy } } + private restoreBreakpoints(instance: MonacoBreakpoint, lineNums: string[]) { + console.log("trying to restore " + lineNums); + this.isUpdatingBreakpoints = true; + instance["lineNumberAndDecorationIdMap"].forEach((v: string, k: number, m: Map) => { + instance["removeSpecifyDecoration"](v, k); + }); + for (let lineNumber of lineNums) { + const range: monaco.IRange = { + startLineNumber: Number(lineNumber), + endLineNumber: Number(lineNumber), + startColumn: 0, + endColumn: 0, + }; + instance["createSpecifyDecoration"](range); + } + this.isUpdatingBreakpoints = false; + } + /** * Specify the co-editor's cursor style. This step is missing from MonacoBinding. * @param coeditor @@ -219,10 +389,11 @@ export class CodeEditorComponent implements AfterViewInit, SafeStyle, OnDestroy switchMap(() => of(this.editorWrapper.getEditor())), catchError(() => of(this.editorWrapper.getEditor())), filter(isDefined), - untilDestroyed(this) + untilDestroyed(this), ) .subscribe((editor: IStandaloneCodeEditor) => { editor.updateOptions({ readOnly: this.formControl.disabled }); + if (!this.code) { return; } @@ -233,9 +404,10 @@ export class CodeEditorComponent implements AfterViewInit, SafeStyle, OnDestroy this.code, editor.getModel()!, new Set([editor]), - this.workflowActionService.getTexeraGraph().getSharedModelAwareness() + this.workflowActionService.getTexeraGraph().getSharedModelAwareness(), ); this.setupAIAssistantActions(editor); + this.setupDebuggingActions(editor); }); } @@ -276,6 +448,127 @@ export class CodeEditorComponent implements AfterViewInit, SafeStyle, OnDestroy this.editorWrapper.initAndStart(userConfig, this.editorElement.nativeElement); } + private onMouseLeftClick(e: EditorMouseEvent, editor:IStandaloneCodeEditor) { + + + const model = editor.getModel()!; + const { type, range, detail, position } = this.getMouseEventTarget(e); + if (model && type === MouseTargetType.GUTTER_GLYPH_MARGIN) { + // This indicates that the current position of the mouse is over the total number of lines in the editor + if (detail.isAfterLines) { + return; + } + const lineNumber = position.lineNumber; + this.udfDebugService.addOrRemoveBreakpoint(this.currentOperatorId, lineNumber); + + const decorationId = + this.instance!["lineNumberAndDecorationIdMap"].get(lineNumber); + + /** + * If a breakpoint exists on the current line, + * it indicates that the current action is to remove the breakpoint + */ + if (decorationId) { + this.instance!["removeSpecifyDecoration"](decorationId, lineNumber); + } else { + this.instance!["createSpecifyDecoration"](range); + } + + + } + } + + private setupDebuggingActions(editor: IStandaloneCodeEditor) { + this.instance = new MonacoBreakpoint({ editor }); + this.instance["createBreakpointDecoration"] = ( + range: Range, + breakpointEnum: BreakpointEnum, + ): { options: editor.IModelDecorationOptions; range: Range } => { + let condition = this.breakpointManager?.getCondition(range.startLineNumber); + let isConditional = false; + if (condition && condition !== "") { + isConditional = true; + } + return { + range, + options: + breakpointEnum === BreakpointEnum.Exist + ? (isConditional ? CONDITIONAL_BREAKPOINT_OPTIONS : BREAKPOINT_OPTIONS) + : BREAKPOINT_HOVER_OPTIONS, + }; + }; + this.instance["mouseDownDisposable"]?.dispose(); + + this.instance["mouseDownDisposable"] = editor.onMouseDown( (evt: EditorMouseEvent) => { + if (evt.event.rightButton) { + return; + } + this.onMouseLeftClick(evt, editor); + } + ); + + (this.instance).on("breakpointChanged", lineNums => { + if (this.isUpdatingBreakpoints) { + return; + } + this.breakpointManager?.setBreakpoints(lineNums.map(n => String(n))); + console.log("breakpointChanged: " + lineNums); + }); + + this.breakpointManager?.getBreakpointHitStream() + .pipe(untilDestroyed(this)) + .subscribe(lineNum => { + console.log("highlight " + lineNum); + this.instance!.removeHighlight(); + if (lineNum != 0) { + this.instance!.setLineHighlight(lineNum); + } + }); + this.breakpointManager?.getLineNumToBreakpointMappingStream().pipe(untilDestroyed(this)).subscribe( + mapping => { + console.log("trigger" + mapping); + this.restoreBreakpoints(this.instance!, Array.from(mapping.keys())); + }); + + editor.onContextMenu((e: EditorMouseEvent) => { + const { type, range, detail, position } = this.getMouseEventTarget(e); + if (type === MouseTargetType.GUTTER_GLYPH_MARGIN) { + + // This indicates that the current position of the mouse is over the total number of lines in the editor + if (detail.isAfterLines) { + return; + } + + // Get the layout info of the editor + const layoutInfo = editor.getLayoutInfo()!; + + // Get the range start line number + const startLineNumber = range.startLineNumber; + + if (!this.instance!["lineNumberAndDecorationIdMap"].has(startLineNumber)) { + return; + } + // Get the top position for the start line number + const topForLineNumber = editor.getTopForLineNumber(startLineNumber); + // Calculate the middle y position for the line number + const lineHeight = editor.getOption(monaco.editor.EditorOption.lineHeight); + const middleForLineNumber = topForLineNumber + lineHeight / 2; + + // Get the editor's DOM node and its bounding rect + const editorDomNode = editor.getDomNode()!; + const editorRect = editorDomNode.getBoundingClientRect(); + + // Calculate x and y positions + const x = editorRect.left + layoutInfo.glyphMarginLeft - editor.getScrollLeft(); + const y = editorRect.top + middleForLineNumber - editor.getScrollTop(); + + this.showTooltip(x, y, startLineNumber, this.breakpointManager!); + } + }); + + + } + private setupAIAssistantActions(editor: IStandaloneCodeEditor) { // Check if the AI provider is "openai" this.aiAssistantService @@ -362,7 +655,7 @@ export class CodeEditorComponent implements AfterViewInit, SafeStyle, OnDestroy currVariable.startLine, currVariable.startColumn + offset, currVariable.endLine, - currVariable.endColumn + offset + currVariable.endColumn + offset, ); const highlight = editor.createDecorationsCollection([ @@ -374,7 +667,7 @@ export class CodeEditorComponent implements AfterViewInit, SafeStyle, OnDestroy className: "annotation-highlight", }, }, - ]); + ])!; this.handleTypeAnnotation(variableCode, variableRange, editor, variableLineNumber, allCode); @@ -404,7 +697,7 @@ export class CodeEditorComponent implements AfterViewInit, SafeStyle, OnDestroy range: monaco.Range, editor: monaco.editor.IStandaloneCodeEditor, lineNumber: number, - allCode: string + allCode: string, ): void { this.aiAssistantService .getTypeAnnotations(code, lineNumber, allCode) @@ -448,7 +741,7 @@ export class CodeEditorComponent implements AfterViewInit, SafeStyle, OnDestroy this.currentRange.startLineNumber, this.currentRange.startColumn, this.currentRange.endLineNumber, - this.currentRange.endColumn + this.currentRange.endColumn, ); this.insertTypeAnnotations(this.editorWrapper.getEditor()!, selection, this.currentSuggestion); @@ -478,7 +771,7 @@ export class CodeEditorComponent implements AfterViewInit, SafeStyle, OnDestroy private insertTypeAnnotations( editor: monaco.editor.IStandaloneCodeEditor, selection: monaco.Selection, - annotations: string + annotations: string, ) { const endLineNumber = selection.endLineNumber; const endColumn = selection.endColumn; @@ -490,4 +783,10 @@ export class CodeEditorComponent implements AfterViewInit, SafeStyle, OnDestroy onFocus() { this.workflowActionService.getJointGraphWrapper().highlightOperators(this.currentOperatorId); } + + onMouseLeave() { + if (this.instance) { + this.instance!["removeHoverDecoration"](); + } + } } diff --git a/core/gui/src/app/workspace/component/menu/menu.component.ts b/core/gui/src/app/workspace/component/menu/menu.component.ts index b671d40d954..dd53600bb9c 100644 --- a/core/gui/src/app/workspace/component/menu/menu.component.ts +++ b/core/gui/src/app/workspace/component/menu/menu.component.ts @@ -31,6 +31,7 @@ import { NzModalService } from "ng-zorro-antd/modal"; import { ResultExportationComponent } from "../result-exportation/result-exportation.component"; import { ReportGenerationService } from "../../service/report-generation/report-generation.service"; import { ShareAccessComponent } from "src/app/dashboard/component/user/share-access/share-access.component"; +import { UdfDebugService } from "../../service/operator-debug/udf-debug.service"; /** * MenuComponent is the top level menu bar that shows * the Texera title and workflow execution button @@ -102,7 +103,8 @@ export class MenuComponent implements OnInit { public operatorMenu: OperatorMenuService, public coeditorPresenceService: CoeditorPresenceService, private modalService: NzModalService, - private reportGenerationService: ReportGenerationService + private reportGenerationService: ReportGenerationService, + private udfDebugService: UdfDebugService, ) { workflowWebsocketService .subscribeToEvent("ExecutionDurationUpdateEvent") @@ -205,6 +207,10 @@ export class MenuComponent implements OnInit { case ExecutionState.Completed: case ExecutionState.Killed: case ExecutionState.Failed: + // assuming there is only one workflow running. + // reset all states. + this.workflowWebsocketService.clearDebugCommands(); + this.udfDebugService.clearDebugStates(); return { text: "Run", icon: "play-circle", diff --git a/core/gui/src/app/workspace/service/execute-workflow/execute-workflow.service.ts b/core/gui/src/app/workspace/service/execute-workflow/execute-workflow.service.ts index b88fb58db34..46e25e81d97 100644 --- a/core/gui/src/app/workspace/service/execute-workflow/execute-workflow.service.ts +++ b/core/gui/src/app/workspace/service/execute-workflow/execute-workflow.service.ts @@ -266,11 +266,11 @@ export class ExecuteWorkflowService { this.workflowWebsocketService.send("WorkflowResumeRequest", {}); } - public skipTuples(workers: ReadonlyArray): void { + public skipTuples(workerIds: ReadonlyArray): void { if (this.currentState.state !== ExecutionState.Paused) { throw new Error("cannot skip tuples, the current execution state is " + this.currentState.state); } - this.workflowWebsocketService.send("SkipTupleRequest", { workers }); + this.workflowWebsocketService.send("SkipTupleRequest", { workerIds }); } public retryExecution(workers: ReadonlyArray): void { diff --git a/core/gui/src/app/workspace/service/operator-debug/udf-debug.service.ts b/core/gui/src/app/workspace/service/operator-debug/udf-debug.service.ts new file mode 100644 index 00000000000..554426db243 --- /dev/null +++ b/core/gui/src/app/workspace/service/operator-debug/udf-debug.service.ts @@ -0,0 +1,273 @@ +import * as Y from "yjs"; +import {Injectable} from "@angular/core"; +import {BehaviorSubject, Subject} from "rxjs"; +import {WorkflowWebsocketService} from "../workflow-websocket/workflow-websocket.service"; +import {FrontendDebugCommand, UDFBreakpointInfo} from "../../types/workflow-websocket.interface"; +import {ExecutionState} from "../../types/execute-workflow.interface"; +import {WorkflowActionService} from "../workflow-graph/model/workflow-action.service"; +import { MonacoBreakpoint } from "monaco-breakpoints"; + +export class BreakpointManager { + + constructor(private workflowWebsocketService:WorkflowWebsocketService, + private currentOperatorId:string, + private lineNumToBreakpointMapping:Y.Map) { + this.triggerUpdate() + this.lineNumToBreakpointMapping.observe(evt => { + this.triggerUpdate() + }) + + workflowWebsocketService.subscribeToEvent("WorkflowStateEvent").subscribe(evt =>{ + if(evt.state === ExecutionState.Initializing){ + if(!this.workflowWebsocketService.executionInitiator){ + this.debugCommandQueue = []; + }else{ + this.sendCommand(); + } + this.executionActive = true; + } + if(evt.state === ExecutionState.Running || evt.state === ExecutionState.Paused){ + this.executionActive = true; + } + }) + + workflowWebsocketService.subscribeToEvent("ConsoleUpdateEvent").subscribe(evt =>{ + if(evt.messages.length === 0){ + return; + } + if(evt.messages[evt.messages.length-1].msgType.name === "DEBUGGER"){ + this.executionActive = true; + this.sendCommand(); + } + }); + } + + public triggerUpdate(){ + console.log("get update ", Array.from(this.lineNumToBreakpointMapping.keys())) + this.lineNumToBreakpointSubject.next(this.lineNumToBreakpointMapping); + } + + private hitLineNum = 0 + private hitLineNumSubject: Subject = new BehaviorSubject(this.hitLineNum) + private lineNumToBreakpointSubject: Subject> = new BehaviorSubject(new Y.Map()); + private debugCommandQueue: FrontendDebugCommand[] = []; + private executionActive = false; + + + + private queueCommand(cmd: FrontendDebugCommand){ + let exist = this.debugCommandQueue.find(value => { + return value == cmd; + }) + if(exist){ + return; + } + if(cmd.command === "clear"){ + const breakCommandIdx = this.debugCommandQueue.findIndex(request => request.command === "break" && request.line == cmd.line); + if (breakCommandIdx !== -1) { + this.debugCommandQueue.splice(breakCommandIdx, 1); + return; + } + } else if(cmd.command === "condition"){ + const breakCommandIdx = this.debugCommandQueue.findIndex(request => request.command === "break" && request.line == cmd.line); + if (breakCommandIdx !== -1) { + this.debugCommandQueue[breakCommandIdx] = {...this.debugCommandQueue[breakCommandIdx], condition: cmd.condition} + return; + } + } + this.debugCommandQueue.push(cmd); + if(this.executionActive){ + this.sendCommand(); + } + } + + private sendCommand(){ + console.log("try to send a command"); + if(this.debugCommandQueue.length > 0){ + let payload = this.debugCommandQueue.shift(); + this.workflowWebsocketService.prepareDebugCommand(payload!); + let needContinue = this.debugCommandQueue.length === 0 || this.debugCommandQueue[this.debugCommandQueue.length-1].command !== "continue"; + if(payload?.command === "break" && this.hitLineNum == 0 && needContinue){ + this.debugCommandQueue.push({...payload, command: "continue"}); + } + } + } + + public getBreakpointHitStream() { + return this.hitLineNumSubject.asObservable(); + } + + public getLineNumToBreakpointMappingStream(){ + return this.lineNumToBreakpointSubject.asObservable(); + } + + public resetState(){ + this.executionActive = false; + this.debugCommandQueue = []; + this.lineNumToBreakpointMapping.forEach((v,k) => { + this.queueCommand({operatorId: this.currentOperatorId, command:"break", line: Number(k), breakpointId:0, condition: v.condition}) + }); + this.setHitLineNum(0); + } + + + public removeBreakpoint(lineNum: number){ + let line = String(lineNum) + this.setBreakpoints(Array.from(this.lineNumToBreakpointMapping.keys()).filter(e => e!= line), false) + } + + public setBreakpoints(lineNum: string[], triggerClear:boolean = true){ + console.log("set breakpoint"+lineNum) + let changed = false; + let lineSet = new Set(lineNum); + lineSet.forEach(line =>{ + if(!this.lineNumToBreakpointMapping.has(line)){ + changed = true; + this.lineNumToBreakpointMapping.set(line, {breakpointId:undefined, condition:""}) + console.log("add break command") + this.queueCommand({operatorId: this.currentOperatorId, + command:"break", + line:Number(line), + breakpointId:0, + condition: ""}) + } + }) + this.lineNumToBreakpointMapping.forEach((v,k,m) =>{ + if(!lineSet.has(k)){ + changed = true; + if(triggerClear){ + this.queueCommand({operatorId: this.currentOperatorId, + command:"clear", + line:Number(k), + breakpointId:v.breakpointId ?? 0, condition:""}); + } + m.delete(k) + } + }) + if(changed){ + this.triggerUpdate(); + } + } + + public getCondition(lineNum:number):string{ + let line = String(lineNum) + if(!this.lineNumToBreakpointMapping.has(line)){ + return ""; + } + let info = this.lineNumToBreakpointMapping.get(line)! + return info.condition; + } + + public setCondition(lineNum:number, condition:string){ + let line = String(lineNum) + let info = this.lineNumToBreakpointMapping.get(line)! + this.lineNumToBreakpointMapping.set(line, {...info, condition:condition}); + this.queueCommand({operatorId: this.currentOperatorId, + command:"condition", + line:lineNum, + breakpointId:info.breakpointId ?? 0, + condition: condition}) + this.triggerUpdate(); + } + + public assignBreakpointId(lineNum: number, breakpointId: number): void { + let line = String(lineNum) + console.log("assign id to breakpoint"+lineNum+" "+breakpointId) + let info = this.lineNumToBreakpointMapping.get(line)! + this.lineNumToBreakpointMapping.set(line, {...info, breakpointId:breakpointId}) + } + + public setHitLineNum(lineNum: number) { + this.hitLineNum = lineNum + this.hitLineNumSubject.next(this.hitLineNum) + } + + setBreakpoint(lineNumber: number) { + + } +} + +@Injectable({ + providedIn: "root", +}) +export class UdfDebugService { + private breakpointManagers: Map = new Map(); + + constructor(private workflowWebsocketService: WorkflowWebsocketService, private workflowActionService:WorkflowActionService) { + this.subscribePythonLineHighlight(); + } + + public getOrCreateManager(operatorId: string): BreakpointManager { + let debugState = this.workflowActionService.texeraGraph.sharedModel.debugState + if(!debugState.has(operatorId)){ + debugState.set(operatorId, new Y.Map()) + } + if (!this.breakpointManagers.has(operatorId)) { + this.breakpointManagers.set(operatorId, new BreakpointManager(this.workflowWebsocketService, operatorId, debugState.get(operatorId))); + } + let mgr = this.breakpointManagers.get(operatorId)!; + mgr.triggerUpdate() + return mgr; + } + + public getAllManagers():BreakpointManager[]{ + return Array.from(this.breakpointManagers.values()); + } + + private subscribePythonLineHighlight() { + this.workflowWebsocketService.subscribeToEvent("ConsoleUpdateEvent").subscribe(event => { + if (event.messages.length == 0) { + return + } + event.messages.forEach(msg => { + console.log("processing message", msg) + const breakpointManager = this.getOrCreateManager(event.operatorId); + if (msg.source == "(Pdb)" && msg.msgType.name == "DEBUGGER") { + if (msg.title.startsWith(">")) { + const lineNum = this.extractLineNumber(msg.title) + if (lineNum) { + breakpointManager.setHitLineNum(lineNum) + } + } + } + if (msg.msgType.name == "ERROR") { + const lineNum = this.extractLineNumberException(msg.source) + console.log(lineNum) + if (lineNum) { + breakpointManager.setHitLineNum(lineNum) + } + } + }) + }) + } + + private extractLineNumber(message: string): number | undefined { + const regex = /\.py\((\d+)\)/; + const match = message.match(regex); + + if (match && match[1]) { + return parseInt(match[1]); + } + + return undefined; + } + + private extractLineNumberException(message: string): number | undefined { + const regex = /:(\d+)/; + const match = message.match(regex); + + if (match && match[1]) { + return parseInt(match[1]); + } + + return undefined; + } + + clearDebugStates() { + this.getAllManagers().forEach(manager => manager.resetState()) + } + + addOrRemoveBreakpoint(operatorId: string, lineNumber: number) { + this.getOrCreateManager(operatorId).setBreakpoint(lineNumber) + } +} diff --git a/core/gui/src/app/workspace/service/workflow-graph/model/shared-model.ts b/core/gui/src/app/workspace/service/workflow-graph/model/shared-model.ts index d8bc204a977..93ef038e8d9 100644 --- a/core/gui/src/app/workspace/service/workflow-graph/model/shared-model.ts +++ b/core/gui/src/app/workspace/service/workflow-graph/model/shared-model.ts @@ -20,6 +20,7 @@ export class SharedModel { public commentBoxMap: Y.Map>; public operatorLinkMap: Y.Map; public elementPositionMap: Y.Map; + public debugState: Y.Map; public undoManager: Y.UndoManager; public clientId: string; @@ -36,6 +37,7 @@ export class SharedModel { public user?: User ) { // Initialize Y-structures. + this.debugState = this.yDoc.getMap("debugActions"); this.operatorIDMap = this.yDoc.getMap("operatorIDMap"); this.commentBoxMap = this.yDoc.getMap("commentBoxMap"); this.operatorLinkMap = this.yDoc.getMap("operatorLinkMap"); diff --git a/core/gui/src/app/workspace/service/workflow-graph/model/workflow-action.service.ts b/core/gui/src/app/workspace/service/workflow-graph/model/workflow-action.service.ts index 72e73857879..b22f3e17c8c 100644 --- a/core/gui/src/app/workspace/service/workflow-graph/model/workflow-action.service.ts +++ b/core/gui/src/app/workspace/service/workflow-graph/model/workflow-action.service.ts @@ -65,7 +65,7 @@ export const DEFAULT_SETTINGS = { providedIn: "root", }) export class WorkflowActionService { - private readonly texeraGraph: WorkflowGraph; + public readonly texeraGraph: WorkflowGraph; private readonly jointGraph: joint.dia.Graph; private readonly jointGraphWrapper: JointGraphWrapper; private readonly syncTexeraModel: SyncTexeraModel; diff --git a/core/gui/src/app/workspace/service/workflow-websocket/workflow-websocket.service.ts b/core/gui/src/app/workspace/service/workflow-websocket/workflow-websocket.service.ts index b105ea40151..967f9338925 100644 --- a/core/gui/src/app/workspace/service/workflow-websocket/workflow-websocket.service.ts +++ b/core/gui/src/app/workspace/service/workflow-websocket/workflow-websocket.service.ts @@ -2,6 +2,7 @@ import { Injectable } from "@angular/core"; import { interval, Observable, Subject, Subscription, timer } from "rxjs"; import { webSocket, WebSocketSubject } from "rxjs/webSocket"; import { + FrontendDebugCommand, TexeraWebsocketEvent, TexeraWebsocketEventTypeMap, TexeraWebsocketEventTypes, @@ -13,6 +14,7 @@ import { delayWhen, filter, map, retryWhen, tap } from "rxjs/operators"; import { environment } from "../../../../environments/environment"; import { AuthService } from "../../../common/service/user/auth.service"; import { getWebsocketUrl } from "src/app/common/util/url"; +import { ExecutionState } from "../../types/execute-workflow.interface"; export const WS_HEARTBEAT_INTERVAL_MS = 10000; export const WS_RECONNECT_INTERVAL_MS = 3000; @@ -29,6 +31,9 @@ export class WorkflowWebsocketService { private websocket?: WebSocketSubject; private wsWithReconnectSubscription?: Subscription; private readonly webSocketResponseSubject: Subject = new Subject(); + private requestQueue: Array = []; + private assignedWorkerIds: Map = new Map(); + public executionInitiator = false; constructor() { // setup heartbeat @@ -56,6 +61,12 @@ export class WorkflowWebsocketService { type, ...payload, } as any as TexeraWebsocketRequest; + if(request.type === "WorkflowKillRequest"){ + this.assignedWorkerIds.clear(); + } + if(request.type === "WorkflowExecuteRequest"){ + this.executionInitiator = true; + } this.websocket?.next(request); } @@ -98,6 +109,16 @@ export class WorkflowWebsocketService { if (evt.type === "ClusterStatusUpdateEvent") { this.numWorkers = evt.numWorkers; } + if(evt.type === "WorkflowStateEvent"){ + if(evt.state === ExecutionState.Completed || evt.state === ExecutionState.Killed || evt.state === ExecutionState.Failed){ + this.assignedWorkerIds.clear(); + this.executionInitiator = false; + } + } + if(evt.type === "WorkerAssignmentUpdateEvent"){ + this.assignedWorkerIds.set(evt.operatorId, evt.workerIds); + this.processQueue(); + } this.isConnected = true; }); } @@ -106,4 +127,69 @@ export class WorkflowWebsocketService { this.closeWebsocket(); this.openWebsocket(wId); } + + + public clearDebugCommands(){ + this.requestQueue = []; + } + + public getWorkerIds(operatorId: string): ReadonlyArray { + return this.assignedWorkerIds.get(operatorId) || []; + } + + + public prepareDebugCommand(payload:FrontendDebugCommand){ + this.requestQueue.push(payload); + this.processQueue(); + } + + private sendDebugCommandRequest(request: FrontendDebugCommand, workerId: string): void { + let cmd: string = ""; + if(request.command === "break"){ + cmd = "break "+request.line; + if(request.condition !== ""){ + cmd += " ,"+request.condition + } + }else if(request.command === "clear"){ + cmd = "clear "+request.breakpointId; + }else if(request.command === "condition"){ + cmd = "condition "+request.breakpointId+" "+request.condition; + }else{ + cmd = request.command + } + console.log("sending", { + operatorId: request.operatorId, + workerId, + cmd, + }); + this.send("DebugCommandRequest", { + operatorId: request.operatorId, + workerId, + cmd, + }); + } + + private processQueue(): void { + // Process the request queue + let initialQueueLength = this.requestQueue.length; + + // Loop through the initial length of the queue to prevent infinite loops with continuously failing items + for (let i = 0; i < initialQueueLength; i++) { + const request = this.requestQueue.shift(); + if (request) { + console.log("got this request", request); + if (this.assignedWorkerIds.has(request.operatorId)) { + const workerIds = this.assignedWorkerIds.get(request.operatorId); + if (workerIds) { + for (let workerId of workerIds) { + this.sendDebugCommandRequest(request, workerId); + } + } + } else { + // If the condition is not met, push the request back to the end of the queue + this.requestQueue.push(request); + } + } + } + } } diff --git a/core/gui/src/app/workspace/types/workflow-websocket.interface.ts b/core/gui/src/app/workspace/types/workflow-websocket.interface.ts index 73ec3758b4c..43718ed1a49 100644 --- a/core/gui/src/app/workspace/types/workflow-websocket.interface.ts +++ b/core/gui/src/app/workspace/types/workflow-websocket.interface.ts @@ -174,6 +174,19 @@ export type WorkflowStateInfo = Readonly<{ state: ExecutionState; }>; +export type FrontendDebugCommand = Readonly<{ + operatorId: string; + command:string; + line: number; + breakpointId:number; + condition:string; +}> + +export type UDFBreakpointInfo = Readonly<{ + breakpointId:number | undefined; + condition:string; +}> + export type TexeraWebsocketRequestTypeMap = { EditingTimeCompilationRequest: LogicalPlan; HeartBeatRequest: {}; @@ -181,7 +194,7 @@ export type TexeraWebsocketRequestTypeMap = { ResultExportRequest: ResultExportRequest; ResultPaginationRequest: PaginationRequest; RetryRequest: { workers: ReadonlyArray }; - SkipTupleRequest: { workers: ReadonlyArray }; + SkipTupleRequest: { workerIds: ReadonlyArray }; WorkflowExecuteRequest: WorkflowExecuteRequest; WorkflowKillRequest: {}; WorkflowPauseRequest: {}; diff --git a/core/gui/yarn.lock b/core/gui/yarn.lock index db405dd5723..03b7aad7d5e 100644 --- a/core/gui/yarn.lock +++ b/core/gui/yarn.lock @@ -9383,6 +9383,11 @@ mobx@~4.14.1: resolved "https://registry.yarnpkg.com/mobx/-/mobx-4.14.1.tgz#0dc5622523363f6f5b467a5a0c4c99846b789748" integrity sha512-Oyg7Sr7r78b+QPYLufJyUmxTWcqeQ96S1nmtyur3QL8SeI6e0TqcKKcxbG+sVJLWANhHQkBW/mDmgG5DDC4fdw== +monaco-breakpoints@0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/monaco-breakpoints/-/monaco-breakpoints-0.1.2.tgz#a612e1ffbd2fbe25dc57c489c2f5dfe8221fde18" + integrity sha512-9h7CNP2g2omh7LkhH9BWSudZt5W1Ahv47XmKsuginkQQUnmtbdDE6Iehe7AFRD3B3nC9c1nWW9YW775LgoPOUQ== + monaco-editor-wrapper@5.5.3: version "5.5.3" resolved "https://registry.yarnpkg.com/monaco-editor-wrapper/-/monaco-editor-wrapper-5.5.3.tgz#32030c4af292e086f318bae58033369c0721a0eb" From 5652fb8ecdaa892272b2bdeee6ca3f2ddcef010b Mon Sep 17 00:00:00 2001 From: Yicong Huang <17627829+Yicong-Huang@users.noreply.github.com> Date: Fri, 18 Oct 2024 16:45:48 -0700 Subject: [PATCH 02/33] simplify --- .../code-editor.component.html | 1 - .../code-editor.component.ts | 258 +++++------- .../component/menu/menu.component.ts | 1 - .../console-frame.component.html | 59 ++- .../console-frame/console-frame.component.ts | 18 +- .../operator-debug/udf-debug.service.ts | 374 +++++++++--------- .../workflow-websocket.service.ts | 58 +-- .../types/workflow-websocket.interface.ts | 9 +- 8 files changed, 353 insertions(+), 425 deletions(-) diff --git a/core/gui/src/app/workspace/component/code-editor-dialog/code-editor.component.html b/core/gui/src/app/workspace/component/code-editor-dialog/code-editor.component.html index 9fc88b56301..63f615a31ac 100644 --- a/core/gui/src/app/workspace/component/code-editor-dialog/code-editor.component.html +++ b/core/gui/src/app/workspace/component/code-editor-dialog/code-editor.component.html @@ -4,7 +4,6 @@ class="box" cdkDrag cdkDragBoundary="body" - (mouseleave)="onMouseLeave()" (focusin)="onFocus()">
; private isMultipleVariables: boolean = false; - public isUpdatingBreakpoints = false; + public isRenderingBreakpoints = false; public instance: MonacoBreakpoint | undefined = undefined; public lastBreakLine = 0; @@ -170,23 +170,22 @@ export class CodeEditorComponent implements AfterViewInit, SafeStyle, OnDestroy .subscribeToEvent("ConsoleUpdateEvent") .pipe(untilDestroyed(this)) .subscribe((pythonConsoleUpdateEvent: ConsoleUpdateEvent) => { - const operatorId = pythonConsoleUpdateEvent.operatorId; - pythonConsoleUpdateEvent.messages - .filter(consoleMessage => consoleMessage.msgType.name === "DEBUGGER") - .map( - consoleMessage => { - console.log(consoleMessage); - const pattern = /^Breakpoint (\d+).*\.py:(\d+)\s*$/; - return consoleMessage.title.match(pattern); - }, - ) - .filter(isDefined) - .forEach(match => { - const breakpoint = Number(match[1]); - const lineNumber = Number(match[2]); - this.breakpointManager?.assignBreakpointId(lineNumber, breakpoint); - }); - + // pythonConsoleUpdateEvent.messages + // .filter(consoleMessage => consoleMessage.msgType.name === "DEBUGGER") + // .map( + // consoleMessage => { + // console.log(consoleMessage); + // const pattern = /^Breakpoint (\d+).*\.py:(\d+)\s*$/; + // return consoleMessage.title.match(pattern); + // }, + // ) + // .filter(isDefined) + // .forEach(match => { + // const breakpoint = Number(match[1]); + // const lineNumber = Number(match[2]); + // this.breakpointManager?.assignBreakpointId(lineNumber, breakpoint); + // }); + // if (pythonConsoleUpdateEvent.messages.length === 0) { return; } @@ -194,17 +193,7 @@ export class CodeEditorComponent implements AfterViewInit, SafeStyle, OnDestroy if (lastMsg.title.startsWith("break")) { this.lastBreakLine = Number(lastMsg.title.split(" ")[1]); } - if (lastMsg.title.startsWith("*** Blank or comment")) { - this.isUpdatingBreakpoints = true; - this.instance!["lineNumberAndDecorationIdMap"].forEach((v: string, k: number, m: Map) => { - if (k === this.lastBreakLine) { - console.log("removing " + k); - this.breakpointManager?.removeBreakpoint(k); - this.instance!["removeSpecifyDecoration"](v, k); - } - }); - this.isUpdatingBreakpoints = false; - } + }); } @@ -297,23 +286,6 @@ export class CodeEditorComponent implements AfterViewInit, SafeStyle, OnDestroy } } - private restoreBreakpoints(instance: MonacoBreakpoint, lineNums: string[]) { - console.log("trying to restore " + lineNums); - this.isUpdatingBreakpoints = true; - instance["lineNumberAndDecorationIdMap"].forEach((v: string, k: number, m: Map) => { - instance["removeSpecifyDecoration"](v, k); - }); - for (let lineNumber of lineNums) { - const range: monaco.IRange = { - startLineNumber: Number(lineNumber), - endLineNumber: Number(lineNumber), - startColumn: 0, - endColumn: 0, - }; - instance["createSpecifyDecoration"](range); - } - this.isUpdatingBreakpoints = false; - } /** * Specify the co-editor's cursor style. This step is missing from MonacoBinding. @@ -448,125 +420,68 @@ export class CodeEditorComponent implements AfterViewInit, SafeStyle, OnDestroy this.editorWrapper.initAndStart(userConfig, this.editorElement.nativeElement); } - private onMouseLeftClick(e: EditorMouseEvent, editor:IStandaloneCodeEditor) { - - - const model = editor.getModel()!; - const { type, range, detail, position } = this.getMouseEventTarget(e); - if (model && type === MouseTargetType.GUTTER_GLYPH_MARGIN) { - // This indicates that the current position of the mouse is over the total number of lines in the editor - if (detail.isAfterLines) { - return; - } - const lineNumber = position.lineNumber; - this.udfDebugService.addOrRemoveBreakpoint(this.currentOperatorId, lineNumber); - - const decorationId = - this.instance!["lineNumberAndDecorationIdMap"].get(lineNumber); - - /** - * If a breakpoint exists on the current line, - * it indicates that the current action is to remove the breakpoint - */ - if (decorationId) { - this.instance!["removeSpecifyDecoration"](decorationId, lineNumber); - } else { - this.instance!["createSpecifyDecoration"](range); - } - - - } - } private setupDebuggingActions(editor: IStandaloneCodeEditor) { + console.log("current bps:", this.udfDebugService.getOrCreateManager(this.currentOperatorId).getCurrentBreakpoints()); this.instance = new MonacoBreakpoint({ editor }); - this.instance["createBreakpointDecoration"] = ( - range: Range, - breakpointEnum: BreakpointEnum, - ): { options: editor.IModelDecorationOptions; range: Range } => { - let condition = this.breakpointManager?.getCondition(range.startLineNumber); - let isConditional = false; - if (condition && condition !== "") { - isConditional = true; - } - return { - range, - options: - breakpointEnum === BreakpointEnum.Exist - ? (isConditional ? CONDITIONAL_BREAKPOINT_OPTIONS : BREAKPOINT_OPTIONS) - : BREAKPOINT_HOVER_OPTIONS, - }; - }; this.instance["mouseDownDisposable"]?.dispose(); - this.instance["mouseDownDisposable"] = editor.onMouseDown( (evt: EditorMouseEvent) => { - if (evt.event.rightButton) { - return; + this.instance["mouseDownDisposable"] = editor.onMouseDown((evt: EditorMouseEvent) => { + const { type, detail, position } = this.getMouseEventTarget(evt); + const model = editor.getModel()!; + if (model && type === MouseTargetType.GUTTER_GLYPH_MARGIN) { + if (detail.isAfterLines) { + return; + } + if (evt.event.rightButton) { + this.onMouseRightClick(evt, position.lineNumber, editor); + } else { + this.onMouseLeftClick(evt, position.lineNumber); + } } - this.onMouseLeftClick(evt, editor); - } + }, ); - (this.instance).on("breakpointChanged", lineNums => { - if (this.isUpdatingBreakpoints) { - return; - } - this.breakpointManager?.setBreakpoints(lineNums.map(n => String(n))); - console.log("breakpointChanged: " + lineNums); - }); + this.breakpointManager?.getLineNumToBreakpointMapping().observe(evt => { + let lineNum: number; + evt.changes.keys.forEach((change, lineNum) => { + switch (change.action) { + case "add": + const addedValue = evt.target.get(lineNum)!; + if (isDefined(addedValue.breakpointId)) { + console.log("adding a breakpoint at ", lineNum); + this.instance!["createSpecifyDecoration"]({ + startLineNumber: Number(lineNum), + endLineNumber: Number(lineNum), + startColumn: 0, + endColumn: 0, + }); + } - this.breakpointManager?.getBreakpointHitStream() - .pipe(untilDestroyed(this)) - .subscribe(lineNum => { - console.log("highlight " + lineNum); - this.instance!.removeHighlight(); - if (lineNum != 0) { - this.instance!.setLineHighlight(lineNum); + break; + case "delete": + const deletedValue = change.oldValue; + if (isDefined(deletedValue.breakpointId)) { + console.log("deleting a breakpoint at ", lineNum); + const decorationId = this.instance!["lineNumberAndDecorationIdMap"].get(Number(lineNum)); + this.instance!["removeSpecifyDecoration"](decorationId, Number(lineNum)); + } + break; + case "update": + // this.setCondition(Number(key), change.oldValue); + console.log(evt.target.get(lineNum)); + const newValue = evt.target.get(lineNum)!; + // if old hit is false and the new hit is true, then set the hit line number + if (newValue.hit) { + this.instance?.setLineHighlight(Number(lineNum)); + } + if (!newValue.hit) { + this.instance?.removeHighlight(); + } + break; } }); - this.breakpointManager?.getLineNumToBreakpointMappingStream().pipe(untilDestroyed(this)).subscribe( - mapping => { - console.log("trigger" + mapping); - this.restoreBreakpoints(this.instance!, Array.from(mapping.keys())); - }); - - editor.onContextMenu((e: EditorMouseEvent) => { - const { type, range, detail, position } = this.getMouseEventTarget(e); - if (type === MouseTargetType.GUTTER_GLYPH_MARGIN) { - - // This indicates that the current position of the mouse is over the total number of lines in the editor - if (detail.isAfterLines) { - return; - } - - // Get the layout info of the editor - const layoutInfo = editor.getLayoutInfo()!; - - // Get the range start line number - const startLineNumber = range.startLineNumber; - - if (!this.instance!["lineNumberAndDecorationIdMap"].has(startLineNumber)) { - return; - } - // Get the top position for the start line number - const topForLineNumber = editor.getTopForLineNumber(startLineNumber); - // Calculate the middle y position for the line number - const lineHeight = editor.getOption(monaco.editor.EditorOption.lineHeight); - const middleForLineNumber = topForLineNumber + lineHeight / 2; - - // Get the editor's DOM node and its bounding rect - const editorDomNode = editor.getDomNode()!; - const editorRect = editorDomNode.getBoundingClientRect(); - - // Calculate x and y positions - const x = editorRect.left + layoutInfo.glyphMarginLeft - editor.getScrollLeft(); - const y = editorRect.top + middleForLineNumber - editor.getScrollTop(); - - this.showTooltip(x, y, startLineNumber, this.breakpointManager!); - } }); - - } private setupAIAssistantActions(editor: IStandaloneCodeEditor) { @@ -784,9 +699,38 @@ export class CodeEditorComponent implements AfterViewInit, SafeStyle, OnDestroy this.workflowActionService.getJointGraphWrapper().highlightOperators(this.currentOperatorId); } - onMouseLeave() { - if (this.instance) { - this.instance!["removeHoverDecoration"](); + + private onMouseLeftClick(e: EditorMouseEvent, lineNum: number) { + + + // This indicates that the current position of the mouse is over the total number of lines in the editor + this.udfDebugService.doModifyBreakpoint(this.currentOperatorId, lineNum); + + } + + + private onMouseRightClick(evt: EditorMouseEvent, lineNum: number, editor: IStandaloneCodeEditor) { + if (!this.instance!["lineNumberAndDecorationIdMap"].has(lineNum)) { + return; } + // Get the layout info of the editor + const layoutInfo = editor.getLayoutInfo()!; + + // Get the top position for the start line number + const topPixel = editor.getTopForLineNumber(lineNum); + + // Calculate the middle y position for the line number + const lineHeight = editor.getOption(monaco.editor.EditorOption.lineHeight); + const middleForLineNumber = topPixel + lineHeight / 2; + + // Get the editor's DOM node and its bounding rect + const editorDomNode = editor.getDomNode()!; + const editorRect = editorDomNode.getBoundingClientRect(); + + // Calculate x and y positions + const x = editorRect.left + layoutInfo.glyphMarginLeft - editor.getScrollLeft(); + const y = editorRect.top + middleForLineNumber - editor.getScrollTop(); + + this.showTooltip(x, y, lineNum, this.breakpointManager!); } } diff --git a/core/gui/src/app/workspace/component/menu/menu.component.ts b/core/gui/src/app/workspace/component/menu/menu.component.ts index dd53600bb9c..b036181450d 100644 --- a/core/gui/src/app/workspace/component/menu/menu.component.ts +++ b/core/gui/src/app/workspace/component/menu/menu.component.ts @@ -210,7 +210,6 @@ export class MenuComponent implements OnInit { // assuming there is only one workflow running. // reset all states. this.workflowWebsocketService.clearDebugCommands(); - this.udfDebugService.clearDebugStates(); return { text: "Run", icon: "play-circle", diff --git a/core/gui/src/app/workspace/component/result-panel/console-frame/console-frame.component.html b/core/gui/src/app/workspace/component/result-panel/console-frame/console-frame.component.html index 1ded24099e9..131bc26e270 100644 --- a/core/gui/src/app/workspace/component/result-panel/console-frame/console-frame.component.html +++ b/core/gui/src/app/workspace/component/result-panel/console-frame/console-frame.component.html @@ -134,31 +134,58 @@ [nzAddOnBefore]="addOnBeforeTemplate" class="console-input-container">
+ + + + + + + + + + + + + - +
diff --git a/core/gui/src/app/workspace/component/result-panel/console-frame/console-frame.component.ts b/core/gui/src/app/workspace/component/result-panel/console-frame/console-frame.component.ts index c7bcf73dda0..b2e493fe84a 100644 --- a/core/gui/src/app/workspace/component/result-panel/console-frame/console-frame.component.ts +++ b/core/gui/src/app/workspace/component/result-panel/console-frame/console-frame.component.ts @@ -9,6 +9,7 @@ import { presetPalettes } from "@ant-design/colors"; import { isDefined } from "../../../../common/util/predicate"; import { WorkflowWebsocketService } from "../../../service/workflow-websocket/workflow-websocket.service"; import { NotificationService } from "../../../../common/service/notification/notification.service"; +import { UdfDebugService } from "../../../service/operator-debug/udf-debug.service"; @UntilDestroy() @Component({ @@ -17,7 +18,7 @@ import { NotificationService } from "../../../../common/service/notification/not styleUrls: ["./console-frame.component.scss"], }) export class ConsoleFrameComponent implements OnInit, OnChanges { - @Input() operatorId?: string; + @Input() operatorId!: string; @Input() consoleInputEnabled?: boolean; @ViewChild(CdkVirtualScrollViewport) viewPort?: CdkVirtualScrollViewport; @ViewChild("consoleList", { read: ElementRef }) listElement?: ElementRef; @@ -47,7 +48,8 @@ export class ConsoleFrameComponent implements OnInit, OnChanges { private executeWorkflowService: ExecuteWorkflowService, private workflowConsoleService: WorkflowConsoleService, private workflowWebsocketService: WorkflowWebsocketService, - private notificationService: NotificationService + private notificationService: NotificationService, + private udfDebugService: UdfDebugService ) {} ngOnChanges(changes: SimpleChanges): void { @@ -159,6 +161,18 @@ export class ConsoleFrameComponent implements OnInit, OnChanges { this.notificationService.error((e as Error).message); } } + public onClickStep(): void { + for (let worker of this.workerIds) { + this.udfDebugService.doStep(this.operatorId, worker); + } + } + + public onClickContinue(): void { + for (let worker of this.workerIds) { + this.udfDebugService.doContinue(this.operatorId, worker); + + } + } getMessageLabel(message: ConsoleMessage): string { return this.labelMapping.get(message.msgType.name) ?? ""; diff --git a/core/gui/src/app/workspace/service/operator-debug/udf-debug.service.ts b/core/gui/src/app/workspace/service/operator-debug/udf-debug.service.ts index 554426db243..1480cd41107 100644 --- a/core/gui/src/app/workspace/service/operator-debug/udf-debug.service.ts +++ b/core/gui/src/app/workspace/service/operator-debug/udf-debug.service.ts @@ -1,244 +1,185 @@ import * as Y from "yjs"; -import {Injectable} from "@angular/core"; -import {BehaviorSubject, Subject} from "rxjs"; -import {WorkflowWebsocketService} from "../workflow-websocket/workflow-websocket.service"; -import {FrontendDebugCommand, UDFBreakpointInfo} from "../../types/workflow-websocket.interface"; -import {ExecutionState} from "../../types/execute-workflow.interface"; -import {WorkflowActionService} from "../workflow-graph/model/workflow-action.service"; -import { MonacoBreakpoint } from "monaco-breakpoints"; +import { Injectable } from "@angular/core"; +import { WorkflowWebsocketService } from "../workflow-websocket/workflow-websocket.service"; +import { DebugCommandRequest, UDFBreakpointInfo } from "../../types/workflow-websocket.interface"; +import { OperatorState } from "../../types/execute-workflow.interface"; +import { WorkflowActionService } from "../workflow-graph/model/workflow-action.service"; +import { isDefined } from "../../../common/util/predicate"; +import { WorkflowStatusService } from "../workflow-status/workflow-status.service"; +import { ExecuteWorkflowService } from "../execute-workflow/execute-workflow.service"; export class BreakpointManager { - constructor(private workflowWebsocketService:WorkflowWebsocketService, - private currentOperatorId:string, - private lineNumToBreakpointMapping:Y.Map) { - this.triggerUpdate() - this.lineNumToBreakpointMapping.observe(evt => { - this.triggerUpdate() - }) - - workflowWebsocketService.subscribeToEvent("WorkflowStateEvent").subscribe(evt =>{ - if(evt.state === ExecutionState.Initializing){ - if(!this.workflowWebsocketService.executionInitiator){ - this.debugCommandQueue = []; - }else{ - this.sendCommand(); - } + + constructor(private workflowWebsocketService: WorkflowWebsocketService, + private workflowStatusService: WorkflowStatusService, + private currentOperatorId: string, + private lineNumToBreakpointMapping: Y.Map) { + + + workflowStatusService.getStatusUpdateStream().subscribe(event => { + if (event[this.currentOperatorId]?.operatorState !== OperatorState.Running || event[this.currentOperatorId]?.operatorState !== OperatorState.Paused) { this.executionActive = true; } - if(evt.state === ExecutionState.Running || evt.state === ExecutionState.Paused){ - this.executionActive = true; + if (event[this.currentOperatorId]?.operatorState === OperatorState.Initializing) { + this.resetState(); } - }) + }); - workflowWebsocketService.subscribeToEvent("ConsoleUpdateEvent").subscribe(evt =>{ - if(evt.messages.length === 0){ + workflowWebsocketService.subscribeToEvent("ConsoleUpdateEvent").subscribe(evt => { + if (evt.messages.length === 0) { return; } - if(evt.messages[evt.messages.length-1].msgType.name === "DEBUGGER"){ - this.executionActive = true; - this.sendCommand(); - } + + evt.messages.forEach(msg => { + if (msg.source == "(Pdb)" && msg.msgType.name == "DEBUGGER") { + console.log("received ", msg.title); + if (msg.title.startsWith(">")) { + const lineNum = this.extractLineNumber(msg.title); + if (isDefined(lineNum)) { + this.setHitLineNum(lineNum); + } + } + if (msg.title.startsWith("Breakpoint")) { + // Handle breakpoint added case + const { breakpointId, lineNum } = this.extractBreakpointInfo(msg.title); + if (isDefined(breakpointId) && isDefined(lineNum)) { + this.addBreakpoint(lineNum, breakpointId, ""); + // You can add more logic here, such as storing the breakpoint ID + } + } + if (msg.title.startsWith("Deleted")) { + // Handle breakpoint removed case + const { breakpointId, lineNum } = this.extractBreakpointInfo(msg.title); + if (isDefined(breakpointId) && isDefined(lineNum)) { + console.log(`Breakpoint removed with ID: ${breakpointId}`); + this.removeBreakpoint(lineNum); + // You can add more logic here, such as removing the breakpoint + } + } + } else if (msg.msgType.name == "ERROR") { + const lineNum = this.extractLineNumberException(msg.source); + if (isDefined(lineNum)) { + this.setHitLineNum(lineNum); + } + } + }); }); - } - public triggerUpdate(){ - console.log("get update ", Array.from(this.lineNumToBreakpointMapping.keys())) - this.lineNumToBreakpointSubject.next(this.lineNumToBreakpointMapping); } - private hitLineNum = 0 - private hitLineNumSubject: Subject = new BehaviorSubject(this.hitLineNum) - private lineNumToBreakpointSubject: Subject> = new BehaviorSubject(new Y.Map()); - private debugCommandQueue: FrontendDebugCommand[] = []; - private executionActive = false; + private debugCommandQueue: DebugCommandRequest[] = []; + private executionActive = false; - private queueCommand(cmd: FrontendDebugCommand){ - let exist = this.debugCommandQueue.find(value => { - return value == cmd; - }) - if(exist){ - return; - } - if(cmd.command === "clear"){ - const breakCommandIdx = this.debugCommandQueue.findIndex(request => request.command === "break" && request.line == cmd.line); - if (breakCommandIdx !== -1) { - this.debugCommandQueue.splice(breakCommandIdx, 1); - return; - } - } else if(cmd.command === "condition"){ - const breakCommandIdx = this.debugCommandQueue.findIndex(request => request.command === "break" && request.line == cmd.line); - if (breakCommandIdx !== -1) { - this.debugCommandQueue[breakCommandIdx] = {...this.debugCommandQueue[breakCommandIdx], condition: cmd.condition} - return; - } - } + private queueCommand(cmd: DebugCommandRequest) { this.debugCommandQueue.push(cmd); - if(this.executionActive){ + if (this.executionActive) { this.sendCommand(); + } else { + console.log("execution is not active"); } } - private sendCommand(){ - console.log("try to send a command"); - if(this.debugCommandQueue.length > 0){ + private sendCommand() { + if (this.debugCommandQueue.length > 0) { let payload = this.debugCommandQueue.shift(); - this.workflowWebsocketService.prepareDebugCommand(payload!); - let needContinue = this.debugCommandQueue.length === 0 || this.debugCommandQueue[this.debugCommandQueue.length-1].command !== "continue"; - if(payload?.command === "break" && this.hitLineNum == 0 && needContinue){ - this.debugCommandQueue.push({...payload, command: "continue"}); - } + this.workflowWebsocketService.sendDebugCommand(payload!); } } - public getBreakpointHitStream() { - return this.hitLineNumSubject.asObservable(); - } - - public getLineNumToBreakpointMappingStream(){ - return this.lineNumToBreakpointSubject.asObservable(); - } - public resetState(){ + public resetState() { this.executionActive = false; this.debugCommandQueue = []; - this.lineNumToBreakpointMapping.forEach((v,k) => { - this.queueCommand({operatorId: this.currentOperatorId, command:"break", line: Number(k), breakpointId:0, condition: v.condition}) - }); + this.lineNumToBreakpointMapping.clear(); this.setHitLineNum(0); } - public removeBreakpoint(lineNum: number){ - let line = String(lineNum) - this.setBreakpoints(Array.from(this.lineNumToBreakpointMapping.keys()).filter(e => e!= line), false) + private hasBreakpoint(lineNum: number): boolean { + return this.lineNumToBreakpointMapping.has(String(lineNum)); } - public setBreakpoints(lineNum: string[], triggerClear:boolean = true){ - console.log("set breakpoint"+lineNum) - let changed = false; - let lineSet = new Set(lineNum); - lineSet.forEach(line =>{ - if(!this.lineNumToBreakpointMapping.has(line)){ - changed = true; - this.lineNumToBreakpointMapping.set(line, {breakpointId:undefined, condition:""}) - console.log("add break command") - this.queueCommand({operatorId: this.currentOperatorId, - command:"break", - line:Number(line), - breakpointId:0, - condition: ""}) - } - }) - this.lineNumToBreakpointMapping.forEach((v,k,m) =>{ - if(!lineSet.has(k)){ - changed = true; - if(triggerClear){ - this.queueCommand({operatorId: this.currentOperatorId, - command:"clear", - line:Number(k), - breakpointId:v.breakpointId ?? 0, condition:""}); - } - m.delete(k) - } - }) - if(changed){ - this.triggerUpdate(); - } - } - public getCondition(lineNum:number):string{ - let line = String(lineNum) - if(!this.lineNumToBreakpointMapping.has(line)){ + public getCondition(lineNum: number): string { + let line = String(lineNum); + if (!this.lineNumToBreakpointMapping.has(line)) { return ""; } - let info = this.lineNumToBreakpointMapping.get(line)! + let info = this.lineNumToBreakpointMapping.get(line)!; return info.condition; } - public setCondition(lineNum:number, condition:string){ - let line = String(lineNum) - let info = this.lineNumToBreakpointMapping.get(line)! - this.lineNumToBreakpointMapping.set(line, {...info, condition:condition}); - this.queueCommand({operatorId: this.currentOperatorId, - command:"condition", - line:lineNum, - breakpointId:info.breakpointId ?? 0, - condition: condition}) - this.triggerUpdate(); - } + public setCondition(lineNum: number, condition: string) { + let line = String(lineNum); + let info = this.lineNumToBreakpointMapping.get(line)!; + this.lineNumToBreakpointMapping.set(line, { ...info, condition: condition }); + this.queueCommand({ + operatorId: this.currentOperatorId, + workerId: "1", + cmd: "condition " + info.breakpointId + " " + info.condition, + }); - public assignBreakpointId(lineNum: number, breakpointId: number): void { - let line = String(lineNum) - console.log("assign id to breakpoint"+lineNum+" "+breakpointId) - let info = this.lineNumToBreakpointMapping.get(line)! - this.lineNumToBreakpointMapping.set(line, {...info, breakpointId:breakpointId}) } - public setHitLineNum(lineNum: number) { - this.hitLineNum = lineNum - this.hitLineNumSubject.next(this.hitLineNum) + public setContinue() { + // for each breakpoint in this.lineNumToBreakpointMapping, if breakpointId is undefined, remove it. and if hit is true, set it to false. + this.lineNumToBreakpointMapping.forEach((value, key) => { + if (value.hit) { + this.lineNumToBreakpointMapping.set(key, { ...value, hit: false }); + } + if (value.breakpointId === undefined) { + this.lineNumToBreakpointMapping.delete(key); + } + }); } - setBreakpoint(lineNumber: number) { + public setHitLineNum(lineNum: number) { + console.log("set hit on ", lineNum); + let line = String(lineNum); + if (!this.lineNumToBreakpointMapping.has(line)) { + this.lineNumToBreakpointMapping.set(line, { breakpointId: undefined, condition: "", hit: true }); + } + let breakpointInfo = this.lineNumToBreakpointMapping.get(line)!; + this.lineNumToBreakpointMapping.set(line, { ...breakpointInfo, hit: true }); } -} -@Injectable({ - providedIn: "root", -}) -export class UdfDebugService { - private breakpointManagers: Map = new Map(); - constructor(private workflowWebsocketService: WorkflowWebsocketService, private workflowActionService:WorkflowActionService) { - this.subscribePythonLineHighlight(); + addOrRemoveBreakpoint(lineNum: number, workerIds: readonly string[]) { + if (this.hasBreakpoint(lineNum)) { + // for each workerId + workerIds.forEach(workerId => { + this.queueCommand({ + operatorId: this.currentOperatorId, + workerId: workerId, + cmd: "clear " + this.lineNumToBreakpointMapping.get(String(lineNum))?.breakpointId, + }); + }); + } else { + // for each workerId + workerIds.forEach(workerId => { + this.queueCommand({ + operatorId: this.currentOperatorId, + workerId: workerId, + cmd: "break " + lineNum, + }); + }); + } } - public getOrCreateManager(operatorId: string): BreakpointManager { - let debugState = this.workflowActionService.texeraGraph.sharedModel.debugState - if(!debugState.has(operatorId)){ - debugState.set(operatorId, new Y.Map()) - } - if (!this.breakpointManagers.has(operatorId)) { - this.breakpointManagers.set(operatorId, new BreakpointManager(this.workflowWebsocketService, operatorId, debugState.get(operatorId))); - } - let mgr = this.breakpointManagers.get(operatorId)!; - mgr.triggerUpdate() - return mgr; + addBreakpoint(lineNum: number, breakpointId: number, condition: string) { + this.lineNumToBreakpointMapping.set(String(lineNum), { breakpointId, condition, hit: false }); } - public getAllManagers():BreakpointManager[]{ - return Array.from(this.breakpointManagers.values()); + removeBreakpoint(lineNum: number) { + this.lineNumToBreakpointMapping.delete(String(lineNum)); } - private subscribePythonLineHighlight() { - this.workflowWebsocketService.subscribeToEvent("ConsoleUpdateEvent").subscribe(event => { - if (event.messages.length == 0) { - return - } - event.messages.forEach(msg => { - console.log("processing message", msg) - const breakpointManager = this.getOrCreateManager(event.operatorId); - if (msg.source == "(Pdb)" && msg.msgType.name == "DEBUGGER") { - if (msg.title.startsWith(">")) { - const lineNum = this.extractLineNumber(msg.title) - if (lineNum) { - breakpointManager.setHitLineNum(lineNum) - } - } - } - if (msg.msgType.name == "ERROR") { - const lineNum = this.extractLineNumberException(msg.source) - console.log(lineNum) - if (lineNum) { - breakpointManager.setHitLineNum(lineNum) - } - } - }) - }) + getCurrentBreakpoints() { + return Array.from(this.lineNumToBreakpointMapping.keys()); } private extractLineNumber(message: string): number | undefined { @@ -248,10 +189,17 @@ export class UdfDebugService { if (match && match[1]) { return parseInt(match[1]); } - return undefined; } + private extractBreakpointInfo(title: string): { breakpointId?: number, lineNum?: number } { + const match = title.match(/(?:Breakpoint|Deleted breakpoint) (\d+) at .+:(\d+)/); + return { + breakpointId: match ? parseInt(match[1], 10) : undefined, + lineNum: match ? parseInt(match[2], 10) : undefined, + }; + } + private extractLineNumberException(message: string): number | undefined { const regex = /:(\d+)/; const match = message.match(regex); @@ -263,11 +211,57 @@ export class UdfDebugService { return undefined; } - clearDebugStates() { - this.getAllManagers().forEach(manager => manager.resetState()) + public getLineNumToBreakpointMapping() { + return this.lineNumToBreakpointMapping; + } +} + +@Injectable({ + providedIn: "root", +}) +export class UdfDebugService { + private breakpointManagers: Map = new Map(); + + constructor( + private workflowWebsocketService: WorkflowWebsocketService, + private workflowActionService: WorkflowActionService, + private workflowStatusService: WorkflowStatusService, + private executeWorkflowService: ExecuteWorkflowService) { + + } + + public getOrCreateManager(operatorId: string): BreakpointManager { + let debugState = this.workflowActionService.texeraGraph.sharedModel.debugState; + if (!debugState.has(operatorId)) { + debugState.set(operatorId, new Y.Map()); + } + if (!this.breakpointManagers.has(operatorId)) { + this.breakpointManagers.set(operatorId, new BreakpointManager(this.workflowWebsocketService, this.workflowStatusService, operatorId, debugState.get(operatorId))); + } + return this.breakpointManagers.get(operatorId)!; } - addOrRemoveBreakpoint(operatorId: string, lineNumber: number) { - this.getOrCreateManager(operatorId).setBreakpoint(lineNumber) + doModifyBreakpoint(operatorId: string, lineNumber: number) { + this.getOrCreateManager(operatorId).addOrRemoveBreakpoint(lineNumber, this.executeWorkflowService.getWorkerIds(operatorId)); + } + + doContinue(operatorId: string, workerId: string) { + this.getOrCreateManager(operatorId).setContinue(); + // TODO: make this queue command + this.workflowWebsocketService.send("DebugCommandRequest", { + operatorId, + workerId, + cmd: "continue", + }); + } + + doStep(operatorId: string, workerId: string) { + this.getOrCreateManager(operatorId).setContinue(); + // TODO: make this queue command + this.workflowWebsocketService.send("DebugCommandRequest", { + operatorId, + workerId, + cmd: "next", + }); } } diff --git a/core/gui/src/app/workspace/service/workflow-websocket/workflow-websocket.service.ts b/core/gui/src/app/workspace/service/workflow-websocket/workflow-websocket.service.ts index 967f9338925..250367923b7 100644 --- a/core/gui/src/app/workspace/service/workflow-websocket/workflow-websocket.service.ts +++ b/core/gui/src/app/workspace/service/workflow-websocket/workflow-websocket.service.ts @@ -2,7 +2,7 @@ import { Injectable } from "@angular/core"; import { interval, Observable, Subject, Subscription, timer } from "rxjs"; import { webSocket, WebSocketSubject } from "rxjs/webSocket"; import { - FrontendDebugCommand, + DebugCommandRequest, TexeraWebsocketEvent, TexeraWebsocketEventTypeMap, TexeraWebsocketEventTypes, @@ -31,7 +31,7 @@ export class WorkflowWebsocketService { private websocket?: WebSocketSubject; private wsWithReconnectSubscription?: Subscription; private readonly webSocketResponseSubject: Subject = new Subject(); - private requestQueue: Array = []; + private requestQueue: Array = []; private assignedWorkerIds: Map = new Map(); public executionInitiator = false; @@ -61,9 +61,6 @@ export class WorkflowWebsocketService { type, ...payload, } as any as TexeraWebsocketRequest; - if(request.type === "WorkflowKillRequest"){ - this.assignedWorkerIds.clear(); - } if(request.type === "WorkflowExecuteRequest"){ this.executionInitiator = true; } @@ -111,14 +108,9 @@ export class WorkflowWebsocketService { } if(evt.type === "WorkflowStateEvent"){ if(evt.state === ExecutionState.Completed || evt.state === ExecutionState.Killed || evt.state === ExecutionState.Failed){ - this.assignedWorkerIds.clear(); this.executionInitiator = false; } } - if(evt.type === "WorkerAssignmentUpdateEvent"){ - this.assignedWorkerIds.set(evt.operatorId, evt.workerIds); - this.processQueue(); - } this.isConnected = true; }); } @@ -133,40 +125,17 @@ export class WorkflowWebsocketService { this.requestQueue = []; } - public getWorkerIds(operatorId: string): ReadonlyArray { - return this.assignedWorkerIds.get(operatorId) || []; - } - public prepareDebugCommand(payload:FrontendDebugCommand){ + public sendDebugCommand(payload:DebugCommandRequest){ this.requestQueue.push(payload); this.processQueue(); } - private sendDebugCommandRequest(request: FrontendDebugCommand, workerId: string): void { - let cmd: string = ""; - if(request.command === "break"){ - cmd = "break "+request.line; - if(request.condition !== ""){ - cmd += " ,"+request.condition - } - }else if(request.command === "clear"){ - cmd = "clear "+request.breakpointId; - }else if(request.command === "condition"){ - cmd = "condition "+request.breakpointId+" "+request.condition; - }else{ - cmd = request.command - } - console.log("sending", { - operatorId: request.operatorId, - workerId, - cmd, - }); - this.send("DebugCommandRequest", { - operatorId: request.operatorId, - workerId, - cmd, - }); + private sendDebugCommandRequest( cmd: DebugCommandRequest): void { + + console.log("sending", cmd); + this.send("DebugCommandRequest", cmd); } private processQueue(): void { @@ -177,18 +146,7 @@ export class WorkflowWebsocketService { for (let i = 0; i < initialQueueLength; i++) { const request = this.requestQueue.shift(); if (request) { - console.log("got this request", request); - if (this.assignedWorkerIds.has(request.operatorId)) { - const workerIds = this.assignedWorkerIds.get(request.operatorId); - if (workerIds) { - for (let workerId of workerIds) { - this.sendDebugCommandRequest(request, workerId); - } - } - } else { - // If the condition is not met, push the request back to the end of the queue - this.requestQueue.push(request); - } + this.sendDebugCommandRequest(request); } } } diff --git a/core/gui/src/app/workspace/types/workflow-websocket.interface.ts b/core/gui/src/app/workspace/types/workflow-websocket.interface.ts index 43718ed1a49..1181595e578 100644 --- a/core/gui/src/app/workspace/types/workflow-websocket.interface.ts +++ b/core/gui/src/app/workspace/types/workflow-websocket.interface.ts @@ -174,17 +174,10 @@ export type WorkflowStateInfo = Readonly<{ state: ExecutionState; }>; -export type FrontendDebugCommand = Readonly<{ - operatorId: string; - command:string; - line: number; - breakpointId:number; - condition:string; -}> - export type UDFBreakpointInfo = Readonly<{ breakpointId:number | undefined; condition:string; + hit:boolean; }> export type TexeraWebsocketRequestTypeMap = { From 8809c3c5f429cb63b46cc8fd139f17c660cc08d6 Mon Sep 17 00:00:00 2001 From: Yicong Huang <17627829+Yicong-Huang@users.noreply.github.com> Date: Fri, 18 Oct 2024 16:49:58 -0700 Subject: [PATCH 03/33] fix format --- .../code-editor.component.scss | 4 +- .../code-editor.component.ts | 53 ++++++++----------- .../component/menu/menu.component.ts | 2 +- .../console-frame.component.html | 10 ++-- .../console-frame/console-frame.component.ts | 1 - .../operator-debug/udf-debug.service.ts | 50 ++++++++--------- .../workflow-websocket.service.ts | 22 ++++---- .../types/workflow-websocket.interface.ts | 8 +-- 8 files changed, 71 insertions(+), 79 deletions(-) diff --git a/core/gui/src/app/workspace/component/code-editor-dialog/code-editor.component.scss b/core/gui/src/app/workspace/component/code-editor-dialog/code-editor.component.scss index 65d508821b0..7bb9232e8f7 100644 --- a/core/gui/src/app/workspace/component/code-editor-dialog/code-editor.component.scss +++ b/core/gui/src/app/workspace/component/code-editor-dialog/code-editor.component.scss @@ -50,7 +50,6 @@ top: -5px; } - .custom-tooltip { position: absolute; background: #333; @@ -102,9 +101,8 @@ font-weight: bold; } - ::ng-deep .cgmr.codicon.monaco-conditional-breakpoint::before { - content: '?'; + content: "?"; position: absolute; top: 50%; left: 50%; diff --git a/core/gui/src/app/workspace/component/code-editor-dialog/code-editor.component.ts b/core/gui/src/app/workspace/component/code-editor-dialog/code-editor.component.ts index a919b0a7748..00f57fc6246 100644 --- a/core/gui/src/app/workspace/component/code-editor-dialog/code-editor.component.ts +++ b/core/gui/src/app/workspace/component/code-editor-dialog/code-editor.component.ts @@ -51,7 +51,6 @@ export const BREAKPOINT_HOVER_OPTIONS: ModelDecorationOptions = { glyphMarginClassName: "monaco-hover-breakpoint", }; - export const LANGUAGE_SERVER_CONNECTION_TIMEOUT_MS = 1000; /** @@ -122,7 +121,7 @@ export class CodeEditorComponent implements AfterViewInit, SafeStyle, OnDestroy public executeWorkflowService: ExecuteWorkflowService, public workflowWebsocketService: WorkflowWebsocketService, public udfDebugService: UdfDebugService, - private renderer: Renderer2, + private renderer: Renderer2 ) { this.currentOperatorId = this.workflowActionService.getJointGraphWrapper().getCurrentHighlightedOperatorIDs()[0]; const operatorType = this.workflowActionService.getTexeraGraph().getOperator(this.currentOperatorId).operatorType; @@ -193,7 +192,6 @@ export class CodeEditorComponent implements AfterViewInit, SafeStyle, OnDestroy if (lastMsg.title.startsWith("break")) { this.lastBreakLine = Number(lastMsg.title.split(" ")[1]); } - }); } @@ -286,7 +284,6 @@ export class CodeEditorComponent implements AfterViewInit, SafeStyle, OnDestroy } } - /** * Specify the co-editor's cursor style. This step is missing from MonacoBinding. * @param coeditor @@ -361,7 +358,7 @@ export class CodeEditorComponent implements AfterViewInit, SafeStyle, OnDestroy switchMap(() => of(this.editorWrapper.getEditor())), catchError(() => of(this.editorWrapper.getEditor())), filter(isDefined), - untilDestroyed(this), + untilDestroyed(this) ) .subscribe((editor: IStandaloneCodeEditor) => { editor.updateOptions({ readOnly: this.formControl.disabled }); @@ -376,7 +373,7 @@ export class CodeEditorComponent implements AfterViewInit, SafeStyle, OnDestroy this.code, editor.getModel()!, new Set([editor]), - this.workflowActionService.getTexeraGraph().getSharedModelAwareness(), + this.workflowActionService.getTexeraGraph().getSharedModelAwareness() ); this.setupAIAssistantActions(editor); this.setupDebuggingActions(editor); @@ -420,27 +417,28 @@ export class CodeEditorComponent implements AfterViewInit, SafeStyle, OnDestroy this.editorWrapper.initAndStart(userConfig, this.editorElement.nativeElement); } - private setupDebuggingActions(editor: IStandaloneCodeEditor) { - console.log("current bps:", this.udfDebugService.getOrCreateManager(this.currentOperatorId).getCurrentBreakpoints()); + console.log( + "current bps:", + this.udfDebugService.getOrCreateManager(this.currentOperatorId).getCurrentBreakpoints() + ); this.instance = new MonacoBreakpoint({ editor }); this.instance["mouseDownDisposable"]?.dispose(); this.instance["mouseDownDisposable"] = editor.onMouseDown((evt: EditorMouseEvent) => { - const { type, detail, position } = this.getMouseEventTarget(evt); - const model = editor.getModel()!; - if (model && type === MouseTargetType.GUTTER_GLYPH_MARGIN) { - if (detail.isAfterLines) { - return; - } - if (evt.event.rightButton) { - this.onMouseRightClick(evt, position.lineNumber, editor); - } else { - this.onMouseLeftClick(evt, position.lineNumber); - } + const { type, detail, position } = this.getMouseEventTarget(evt); + const model = editor.getModel()!; + if (model && type === MouseTargetType.GUTTER_GLYPH_MARGIN) { + if (detail.isAfterLines) { + return; } - }, - ); + if (evt.event.rightButton) { + this.onMouseRightClick(evt, position.lineNumber, editor); + } else { + this.onMouseLeftClick(evt, position.lineNumber); + } + } + }); this.breakpointManager?.getLineNumToBreakpointMapping().observe(evt => { let lineNum: number; @@ -570,7 +568,7 @@ export class CodeEditorComponent implements AfterViewInit, SafeStyle, OnDestroy currVariable.startLine, currVariable.startColumn + offset, currVariable.endLine, - currVariable.endColumn + offset, + currVariable.endColumn + offset ); const highlight = editor.createDecorationsCollection([ @@ -612,7 +610,7 @@ export class CodeEditorComponent implements AfterViewInit, SafeStyle, OnDestroy range: monaco.Range, editor: monaco.editor.IStandaloneCodeEditor, lineNumber: number, - allCode: string, + allCode: string ): void { this.aiAssistantService .getTypeAnnotations(code, lineNumber, allCode) @@ -656,7 +654,7 @@ export class CodeEditorComponent implements AfterViewInit, SafeStyle, OnDestroy this.currentRange.startLineNumber, this.currentRange.startColumn, this.currentRange.endLineNumber, - this.currentRange.endColumn, + this.currentRange.endColumn ); this.insertTypeAnnotations(this.editorWrapper.getEditor()!, selection, this.currentSuggestion); @@ -686,7 +684,7 @@ export class CodeEditorComponent implements AfterViewInit, SafeStyle, OnDestroy private insertTypeAnnotations( editor: monaco.editor.IStandaloneCodeEditor, selection: monaco.Selection, - annotations: string, + annotations: string ) { const endLineNumber = selection.endLineNumber; const endColumn = selection.endColumn; @@ -699,16 +697,11 @@ export class CodeEditorComponent implements AfterViewInit, SafeStyle, OnDestroy this.workflowActionService.getJointGraphWrapper().highlightOperators(this.currentOperatorId); } - private onMouseLeftClick(e: EditorMouseEvent, lineNum: number) { - - // This indicates that the current position of the mouse is over the total number of lines in the editor this.udfDebugService.doModifyBreakpoint(this.currentOperatorId, lineNum); - } - private onMouseRightClick(evt: EditorMouseEvent, lineNum: number, editor: IStandaloneCodeEditor) { if (!this.instance!["lineNumberAndDecorationIdMap"].has(lineNum)) { return; diff --git a/core/gui/src/app/workspace/component/menu/menu.component.ts b/core/gui/src/app/workspace/component/menu/menu.component.ts index b036181450d..64b9d841d8c 100644 --- a/core/gui/src/app/workspace/component/menu/menu.component.ts +++ b/core/gui/src/app/workspace/component/menu/menu.component.ts @@ -104,7 +104,7 @@ export class MenuComponent implements OnInit { public coeditorPresenceService: CoeditorPresenceService, private modalService: NzModalService, private reportGenerationService: ReportGenerationService, - private udfDebugService: UdfDebugService, + private udfDebugService: UdfDebugService ) { workflowWebsocketService .subscribeToEvent("ExecutionDurationUpdateEvent") diff --git a/core/gui/src/app/workspace/component/result-panel/console-frame/console-frame.component.html b/core/gui/src/app/workspace/component/result-panel/console-frame/console-frame.component.html index 131bc26e270..a892572a366 100644 --- a/core/gui/src/app/workspace/component/result-panel/console-frame/console-frame.component.html +++ b/core/gui/src/app/workspace/component/result-panel/console-frame/console-frame.component.html @@ -154,11 +154,11 @@ nz-tooltip nzTooltipTitle="Retry the faulty Tuple" nz-button> - - + + Retry Tuple