diff --git a/src/benchmark/Sieve.ts b/src/benchmark/Sieve.ts index 436936e9e..6f3c1c1b3 100644 --- a/src/benchmark/Sieve.ts +++ b/src/benchmark/Sieve.ts @@ -9,7 +9,8 @@ import { App, TimeValue, Origin, - Log + Log, + GraphDebugLogger } from "../core/internal"; Log.global.level = Log.levels.INFO; @@ -21,7 +22,7 @@ class Ramp extends Reactor { value = new OutPort(this); constructor(parent: Reactor, until = 100000, period: TimeValue) { - super(parent); + super(parent, "Ramp"); this.until = new Parameter(until); this.next = new Action(this, Origin.logical, period); this.addReaction( @@ -56,7 +57,7 @@ class Filter extends Reactor { hasChild: State; constructor(parent: Reactor, startPrime: number, numberOfPrimes: number) { - super(parent); + super(parent, `FilterFor${startPrime}`); // console.log("Created filter with prime: " + prime) this.startPrime = new Parameter(startPrime); this.localPrimes = new State(new Array()); @@ -87,17 +88,24 @@ class Filter extends Reactor { if (size < numberOfPrimes) { seen.push(p); console.log(`Found new prime number ${p}`); + if (!primes.has(p)) { + ; + } else { + primes.delete(p); + } } else { // Potential prime found. if (!hasChild.get()) { - const n = new Filter(this.getReactor(), p, numberOfPrimes); + const n = this.getReactor()._uncheckedAddSibling(Filter, p, numberOfPrimes); // this.start(n) // console.log("CREATING...") // let x = this.create(Filter, [this.getReactor(), p]) // console.log("CREATED: " + x._getFullyQualifiedName()) // FIXME: weird hack. Maybe just accept writable ports as well? const port = (out as unknown as WritablePort).getPort(); + console.log("connecting......"); this.connect(port, n.inp); + printSieveGraph(); // FIXME: this updates the dependency graph, but it doesn't redo the topological sort // For a pipeline like this one, it is not necessary, but in general it is. // Can we avoid redoing the entire sort? @@ -126,12 +134,28 @@ class Sieve extends App { success?: () => void, fail?: () => void ) { - super(timeout, keepAlive, fast, success, fail); + super(timeout, keepAlive, fast, success, fail, name); this.source = new Ramp(this, 100000, TimeValue.nsec(1)); this.filter = new Filter(this, 2, 1000); this._connect(this.source.value, this.filter.inp); } } -const sieve = new Sieve("Sieve"); + +const sieve = new Sieve("Sieve", undefined, undefined, undefined, ()=>{globalThis.graphDebugLogger?.write("debug0.json")}); + +const printSieveGraph = (): void => { + const graph = sieve["_getPrecedenceGraph"](); + const hierarchy = sieve._getNodeHierarchyLevels(); + const str = graph.toMermaidString(undefined, hierarchy); + const time = sieve["util"].getElapsedLogicalTime(); + console.log(str); + console.log(time); +} + +globalThis.graphDebugLogger = new GraphDebugLogger(sieve); +globalThis.recording = false; + +const primes = new Set([2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, 101, 103, 107, 109, 113, 127, 131, 137, 139, 149, 151, 157, 163, 167, 173, 179, 181, 191, 193, 197, 199, 211, 223, 227, 229, 233, 239, 241, 251, 257, 263, 269, 271, 277, 281, 283, 293, 307, 311, 313, 317, 331, 337, 347, 349, 353, 359, 367, 373, 379, 383, 389, 397, 401, 409, 419, 421, 431, 433, 439, 443, 449, 457, 461, 463, 467, 479, 487, 491, 499, 503, 509, 521, 523, 541, 547, 557, 563, 569, 571, 577, 587, 593, 599, 601, 607, 613, 617, 619, 631, 641, 643, 647, 653, 659, 661, 673, 677, 683, 691, 701, 709, 719, 727, 733, 739, 743, 751, 757, 761, 769, 773, 787, 797, 809, 811, 821, 823, 827, 829, 839, 853, 857, 859, 863, 877, 881, 883, 887, 907, 911, 919, 929, 937, 941, 947, 953, 967, 971, 977, 983, 991, 9972, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, 101, 103, 107, 109, 113, 127, 131, 137, 139, 149, 151, 157, 163, 167, 173, 179, 181, 191, 193, 197, 199, 211, 223, 227, 229, 233, 239, 241, 251, 257, 263, 269, 271, 277, 281, 283, 293, 307, 311, 313, 317, 331, 337, 347, 349, 353, 359, 367, 373, 379, 383, 389, 397, 401, 409, 419, 421, 431, 433, 439, 443, 449, 457, 461, 463, 467, 479, 487, 491, 499, 503, 509, 521, 523, 541, 547, 557, 563, 569, 571, 577, 587, 593, 599, 601, 607, 613, 617, 619, 631, 641, 643, 647, 653, 659, 661, 673, 677, 683, 691, 701, 709, 719, 727, 733, 739, 743, 751, 757, 761, 769, 773, 787, 797, 809, 811, 821, 823, 827, 829, 839, 853, 857, 859, 863, 877, 881, 883, 887, 907, 911, 919, 929, 937, 941, 947, 953, 967, 971, 977, 983, 991, 997]); + sieve._start(); diff --git a/src/core/component.ts b/src/core/component.ts index 2ec876b07..55e16f59a 100644 --- a/src/core/component.ts +++ b/src/core/component.ts @@ -1,5 +1,6 @@ import type {Runtime} from "./internal"; import {Reactor, App, MultiPort, IOPort, Bank} from "./internal"; +import {v4 as uuidv4} from "uuid"; /** * Base class for named objects embedded in a hierarchy of reactors. Each @@ -17,7 +18,7 @@ export abstract class Component { * A symbol that identifies this component, and it also used to selectively * grant access to its privileged functions. */ - protected _key = Symbol("Unique component identifier"); + protected _key = Symbol(uuidv4()); /** * The container of this component. Each component is contained by a @@ -186,11 +187,9 @@ export abstract class Component { public _getName(): string { let name; - if (this instanceof App) { + if (this instanceof Reactor) { name = this._name; - } else { - name = Component.keyOfMatchingEntry(this, this._container); - } + } if (name === "" && this instanceof IOPort) { name = Component.keyOfMatchingMultiport(this, this._container); @@ -200,7 +199,7 @@ export abstract class Component { name = Component.keyOfMatchingBank(this, this._container); } - if (name !== "") { + if (name != null && name !== "") { return name; } else { return this.constructor.name; diff --git a/src/core/graph.ts b/src/core/graph.ts index 3a3a72ae2..d0cbe8383 100644 --- a/src/core/graph.ts +++ b/src/core/graph.ts @@ -3,13 +3,56 @@ * @author Marten Lohstroh */ +import { DebugLogger } from "util"; import {Reaction} from "./reaction"; import type {Sortable, Variable} from "./types"; -import {Log} from "./util"; +import {GraphDebugLogger, Log} from "./util"; + +// TODO: find a way to to this with decorators. +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type, @typescript-eslint/no-explicit-any +/* const debugLoggerDecorator = (target: any, context: ClassMethodDecoratorContext) => { + if (context.kind === "method") { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return function (this: any, ...args: unknown[]): any { + console.log(`${context.name.toString()} is called.`); + console.log(`Tracestack: ${(new Error()).stack}`); + // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access + return target.call(this, ...args); + } + } +} */ + +declare global { + // eslint-disable-next-line no-var + var graphDebugLogger: GraphDebugLogger | undefined; + // eslint-disable-next-line no-var + var recording: boolean; +} + +const debugHelper = (stacktrace: Error): void => { + if (globalThis.recording) { return; } + // If recording now, do not record any subsequent operations, + // as recursion hell might involve when calling `capture`, + // causing infinite loop. + globalThis.recording = true; + if (globalThis.graphDebugLogger == null) { + return + } + + const debuglogger = globalThis.graphDebugLogger; + debuglogger.capture(stacktrace); + globalThis.recording = false; +}; /** * A generic precedence graph. */ + +export interface HierarchyGraphLevel { + name: string, + nodes: T[], + childrenLevels: Array>, +} export class PrecedenceGraph { /** * A map from nodes to the set of their upstream neighbors. @@ -47,6 +90,7 @@ export class PrecedenceGraph { * @param node */ addNode(node: T): void { + debugHelper(new Error("addNode")); if (!this.adjacencyMap.has(node)) { this.adjacencyMap.set(node, new Set()); } @@ -137,6 +181,7 @@ export class PrecedenceGraph { * @param downstream The node at which the directed edge ends. */ addEdge(upstream: T, downstream: T): void { + debugHelper(new Error("addEdge")); const deps = this.adjacencyMap.get(downstream); if (deps == null) { this.adjacencyMap.set(downstream, new Set([upstream])); @@ -189,10 +234,10 @@ export class PrecedenceGraph { * @param edgesWithIssue An array containing arrays with [origin, effect]. * Denotes edges in the graph that causes issues to the execution, will be visualized as `--x` in mermaid. */ - toMermaidString(edgesWithIssue?: Array<[T, T]>): string { + toMermaidString(edgesWithIssue?: Array<[T, T]>, hierarchy?: HierarchyGraphLevel, uniqueNames?: boolean): string { if (edgesWithIssue == null) edgesWithIssue = []; - let result = "graph"; - const nodeToNumber = new Map(); + let result = "graph\n"; + const nodeToSymbolString = new Map(); const getNodeString = (node: T, def: string): string => { if (node == null || node?.toString === Object.prototype.toString) { console.error( @@ -205,27 +250,46 @@ export class PrecedenceGraph { return node.toString(); }; - // Build a block here since we only need `counter` temporarily here + if (hierarchy != null) { + let counter = 0; + let subgraphCounter = 0; + const recurse = (h: HierarchyGraphLevel, level: number): void => { + const indent = " ".repeat(level); + result += level === 0 ? "" : `${indent}subgraph "${(uniqueNames ?? false) ? h.name : `sg${subgraphCounter++}`}"\n`; + for (const v of h.nodes) { + result += `${indent} ${counter}["${getNodeString(v, String(counter))}"]\n` + nodeToSymbolString.set(v, `${counter++}`); + } + for (const c of h.childrenLevels) { + recurse(c, level + 1); + } + result += level === 0 ? "" : `${indent}end\n`; + } + recurse(hierarchy, 0); + } + + // Build a block here since we only need `counter` temporarily here // We use numbers instead of names of reactors directly as node names // in mermaid.js because mermaid has strict restrictions regarding // what could be used as names of the node. { let counter = 0; for (const v of this.getNodes()) { - result += `\n${counter}["${getNodeString(v, String(counter))}"]`; - nodeToNumber.set(v, counter++); + if (nodeToSymbolString.has(v)) { continue; } + result += `\nmissing${counter}["${getNodeString(v, String(counter))}"]`; + nodeToSymbolString.set(v, `missing${counter++}`); } } // This is the effect for (const s of this.getNodes()) { // This is the origin for (const t of this.getUpstreamNeighbors(s)) { - result += `\n${nodeToNumber.get(t)}`; + result += `\n${nodeToSymbolString.get(t)}`; result += edgesWithIssue.some((v) => v[0] === t && v[1] === s) ? " --x " : " --> "; - result += `${nodeToNumber.get(s)}`; + result += `${nodeToSymbolString.get(s)}`; } } return result; diff --git a/src/core/port.ts b/src/core/port.ts index e6206e821..157e30420 100644 --- a/src/core/port.ts +++ b/src/core/port.ts @@ -103,6 +103,10 @@ export abstract class IOPort extends Port { } } + public checkKey(key: symbol | undefined): boolean { + return this._key === key; + } + /** * Only the holder of the key may obtain a writable port. * @param key diff --git a/src/core/reaction.ts b/src/core/reaction.ts index 2dc06580e..5424b6d2f 100644 --- a/src/core/reaction.ts +++ b/src/core/reaction.ts @@ -67,7 +67,8 @@ export class Reaction private deadline?: TimeValue, private readonly late: (...args: ArgList) => void = () => { Log.global.warn("Deadline violation occurred!"); - } + }, + readonly name?: string ) {} /** @@ -181,9 +182,9 @@ export class Reaction * Return string representation of the reaction. */ public toString(): string { - return `${this.reactor._getFullyQualifiedName()}[R${this.reactor._getReactionIndex( - this as unknown as Reaction - )}]`; + return `${this.reactor._getFullyQualifiedName()}` + + ((this.name != null) ? `${this.name} aka` : "") + + `[R${this.reactor._getReactionIndex(this as unknown as Reaction)}]`; } } @@ -199,9 +200,10 @@ export class Mutation extends Reaction { args: [...ArgList], react: (...args: ArgList) => void, deadline?: TimeValue, - late?: (...args: ArgList) => void + late?: (...args: ArgList) => void, + name?: string ) { - super(__parent__, sandbox, trigs, args, react, deadline, late); + super(__parent__, sandbox, trigs, args, react, deadline, late, name); this.parent = __parent__; } @@ -209,8 +211,8 @@ export class Mutation extends Reaction { * @override */ public toString(): string { - return `${this.parent._getFullyQualifiedName()}[M${this.parent._getReactionIndex( - this as unknown as Reaction - )}]`; + return `${this.parent._getFullyQualifiedName()}` + + ((this.name != null) ? `${this.name} aka` : "") + + `[M${this.parent._getReactionIndex(this as unknown as Reaction)}]`; } } diff --git a/src/core/reactor.ts b/src/core/reactor.ts index 806bf2af7..3a9e3adf2 100644 --- a/src/core/reactor.ts +++ b/src/core/reactor.ts @@ -42,7 +42,8 @@ import { Startup, Shutdown, WritableMultiPort, - Dummy + Dummy, + HierarchyGraphLevel } from "./internal"; import {v4 as uuidv4} from "uuid"; import {Bank} from "./bank"; @@ -435,7 +436,11 @@ export abstract class Reactor extends Component { if (src instanceof CallerPort && dst instanceof CalleePort) { this.reactor._connectCall(src, dst); } else if (src instanceof IOPort && dst instanceof IOPort) { - this.reactor._connect(src, dst); + if (this.reactor.canConnect(src, dst) === 2) { + this.reactor._elevatedConnect(src, dst); + } else { + this.reactor._connect(src, dst); + } } else { // ERROR } @@ -483,12 +488,15 @@ export abstract class Reactor extends Component { } }; + _name: string; + /** * Create a new reactor. * @param container The container of this reactor. */ - constructor(container: Reactor | null) { + constructor(container: Reactor | null, name? : string) { super(container); + this._name = (name == null) ? this._key.description?.slice(0, 8) ?? "unknown reactor" : name; this._bankIndex = -1; if (container !== null) { const index = Bank.initializationMap.get(container); @@ -785,7 +793,8 @@ export abstract class Reactor extends Component { deadline?: TimeValue, late: (this: ReactionSandbox, ...args: ArgList) => void = () => { Log.global.warn("Deadline violation occurred!"); - } + }, + name?: string ): void { const calleePorts = trigs.filter((trig) => trig instanceof CalleePort); @@ -799,7 +808,8 @@ export abstract class Reactor extends Component { args, react, deadline, - late + late, + name ); if (trigs.length > 1) { // A procedure can only have a single trigger. @@ -823,7 +833,8 @@ export abstract class Reactor extends Component { args, react, deadline, - late + late, + name ); // Stage it directly if it to be triggered immediately. if (reaction.isTriggeredImmediately()) { @@ -844,7 +855,8 @@ export abstract class Reactor extends Component { deadline?: TimeValue, late: (this: MutationSandbox, ...args: ArgList) => void = () => { Log.global.warn("Deadline violation occurred!"); - } + }, + name?: string ): void { const mutation = new Mutation( this, @@ -853,7 +865,8 @@ export abstract class Reactor extends Component { args, react, deadline, - late + late, + name ); // Stage it directly if it to be triggered immediately. if (mutation.isTriggeredImmediately()) { @@ -940,6 +953,35 @@ export abstract class Reactor extends Component { return graph; } + public _getNodeHierarchyLevels(depth = -1): HierarchyGraphLevel | Reaction> { + this._addHierarchicalDependencies(); + this._addRPCDependencies(); + + const hierarchy: HierarchyGraphLevel | Reaction> = { + // names could be duplicate which mermaid don't like, better be unique + name: `${this._getFullyQualifiedName()}`, + // I think _getReactions and _getMutations might contain children reactions. + // So filter by owner might be needed? + nodes: ([...this._findOwnPorts()] as Array<(Port | Reaction)>) + // reactor is private so we must use bracket + // eslint-disable-next-line @typescript-eslint/dot-notation + .concat([...this._getReactions()].filter((x) => (x["reactor"] === this))) + // eslint-disable-next-line @typescript-eslint/dot-notation + .concat([...this._getMutations()].filter((x) => (x["reactor"] === this))), + childrenLevels: [] + }; + + if (depth !== 0) { + // Sometimes there's duplicative children?? + for (const r of this._getOwnReactors()) { + if (r._getContainer() === this) { + hierarchy.childrenLevels.push(r._getNodeHierarchyLevels(depth - 1)); + } + } + } + return hierarchy; + } + /** * Return the reactors that this reactor owns. */ @@ -1091,7 +1133,7 @@ export abstract class Reactor extends Component { * @param src The start point of a new connection. * @param dst The end point of a new connection. */ - public canConnect(src: IOPort, dst: IOPort): boolean { + public canConnect(src: IOPort, dst: IOPort): boolean | number { // Immediate rule out trivial self loops. if (src === dst) { throw Error("Source port and destination port are the same."); @@ -1105,6 +1147,12 @@ export abstract class Reactor extends Component { throw Error("Destination port is already occupied."); } + if (! (src.checkKey(this._key) && dst.checkKey(this._key) )) { + // FIXME: dirty hack here + // Scoping issue. Does not possess valid key for src/dst. + return 2; + } + if (!this._runtime.isRunning()) { // console.log("Connecting before running") // Validate connections between callers and callees. @@ -1226,10 +1274,10 @@ export abstract class Reactor extends Component { dst: IOPort ): void { Log.debug(this, () => `connecting ${src} and ${dst}`); - // Add dependency implied by connection to local graph. - this._dependencyGraph.addEdge(src, dst); // Register receiver for value propagation. const writer = dst.asWritable(this._getKey(dst)); + // Add dependency implied by connection to local graph. + this._dependencyGraph.addEdge(src, dst); src .getManager(this._getKey(src)) .addReceiver(writer as unknown as WritablePort); @@ -1555,6 +1603,45 @@ export abstract class Reactor extends Component { toString(): string { return this._getFullyQualifiedName(); } + + public _uncheckedAddChild( + constructor:new (container: Reactor, ...args: G) => R, + ...args: G + ): R { + const newReactor = new constructor(this, ...args); + return newReactor; + } + + public _uncheckedAddSibling( + constructor:new (container: Reactor, ...args: G) => R, + ...args: G + ): R { + if (this._getContainer() == null) { + throw new Error(`Reactor ${this} does not have a parent. Sibling is not well-defined.`); + } + if (this._getContainer() === this) { + throw new Error(`Reactor ${this} is self-contained. Adding sibling creates logical issue.`); + } + return this._getContainer()._uncheckedAddChild(constructor, ...args); + } + + public _elevatedConnect(...args: Parameters): ReturnType { + // eslint-disable-next-line @typescript-eslint/no-this-alias + let currentLevel: Reactor = this; + let atTopLevel = false; + while (currentLevel != null && !atTopLevel) { + if (currentLevel === currentLevel._getContainer()) { + atTopLevel = true; + } + try { + currentLevel._connect(...args); + return + } catch (error) { + } + currentLevel = currentLevel._getContainer(); + } + throw new Error(`[DEBUG] _elevatedConnect: Elevated connect failed for ${this._getFullyQualifiedName()}.`); + } } /* @@ -2193,8 +2280,6 @@ export class App extends Reactor { private readonly snooze: Action; - readonly _name: string; - /** * Create a new top-level reactor. * @param executionTimeout Optional parameter to let the execution of the app time out. @@ -2208,18 +2293,18 @@ export class App extends Reactor { keepAlive = false, fast = false, public success: () => void = () => undefined, - public failure: () => void = () => undefined + public failure: () => void = () => undefined, + name?: string ) { - super(null); - - let name = this.constructor.name; - if (name === "") { - name = "app"; - } else { - name = name.charAt(0).toLowerCase() + name.slice(1); + super(null, name); + + if (name == null) { + name = this.constructor.name; + if (name !== "") { + name = name.charAt(0).toLowerCase() + name.slice(1); + } + this._name = name; } - this._name = name; - // Update pointer to runtime object for this reactor and // its startup and shutdown action since the inner class // instance this.__runtime isn't initialized up until here. diff --git a/src/core/util.ts b/src/core/util.ts index 34c9cd87f..88a3c2149 100644 --- a/src/core/util.ts +++ b/src/core/util.ts @@ -1,4 +1,8 @@ import ULog from "ulog"; +import { TimeValue } from "./time"; +import { Component } from "./component"; +import { Reactor } from "./reactor"; +import fs from 'fs'; /** * Utilities for the reactor runtime. @@ -184,3 +188,36 @@ export class Log { } } } +interface runtimeLog { + time: TimeValue, + graphMermaidString: string, + stacktrace: string, +} + +export class GraphDebugLogger { + logs: runtimeLog[]; + rootReactor: Reactor; + + constructor(rootReactor: Reactor) { + this.logs = []; + this.rootReactor = rootReactor; + } + + public capture(stacktrace?: Error): void { + const hierarchy = this.rootReactor._getNodeHierarchyLevels(); + // _getPrecedenceGraph is private + const graph = this.rootReactor["_getPrecedenceGraph"](); + const str = graph.toMermaidString(undefined, hierarchy); + const time = this.rootReactor["util"].getElapsedLogicalTime(); + this.logs.push({ + time, + graphMermaidString: str, + stacktrace: stacktrace?.stack ?? "unknown" + }); + } + + public write(filepath: string): void { + // Blocking + fs.appendFileSync(filepath, JSON.stringify(this.logs), {flag: "a+"}); + } +} \ No newline at end of file