diff --git a/Source/Components/Interfaces/DebugService/DebugCore.h b/Source/Components/Interfaces/DebugService/DebugCore.h index abab7fe..a7ce8a7 100644 --- a/Source/Components/Interfaces/DebugService/DebugCore.h +++ b/Source/Components/Interfaces/DebugService/DebugCore.h @@ -28,6 +28,7 @@ struct DebugSignalInfo { volatile bool isTracing; volatile bool isForcing; uint8 forcedValue[1024]; + uint8 forcedMask[32]; // bit e set → element e is forced; supports up to 256 elements uint32 internalID; volatile uint32 decimationFactor; volatile uint32 decimationCounter; diff --git a/Source/Components/Interfaces/DebugService/DebugServiceBase.cpp b/Source/Components/Interfaces/DebugService/DebugServiceBase.cpp index 082fad1..601f97e 100644 --- a/Source/Components/Interfaces/DebugService/DebugServiceBase.cpp +++ b/Source/Components/Interfaces/DebugService/DebugServiceBase.cpp @@ -64,16 +64,34 @@ static void EscapeJson(const char8 *src, StreamString &dst) { } } -static bool SuffixMatch(const char8 *target, const char8 *pattern) { - uint32 tLen = StringHelper::Length(target); - uint32 pLen = StringHelper::Length(pattern); - if (pLen > tLen) +// Returns true if `str` ends with `suffix` at a word boundary (i.e. the char +// immediately before the match is '.', or the match covers the entire string). +static bool SuffixMatch(const char8 *str, const char8 *suffix) { + uint32 sLen = StringHelper::Length(str); + uint32 sufLen = StringHelper::Length(suffix); + if (sufLen > sLen) return false; - const char8 *suffix = target + (tLen - pLen); - if (StringHelper::Compare(suffix, pattern) == 0) { - if (tLen == pLen || *(suffix - 1) == '.') - return true; - } + const char8 *tail = str + (sLen - sufLen); + if (StringHelper::Compare(tail, suffix) != 0) + return false; + return (sLen == sufLen || *(tail - 1u) == '.'); +} + +// Match a stored alias against a user-supplied name. +// Three cases are tried in order: +// 1. Exact match ("App.DS.Sig" == "App.DS.Sig") +// 2. Alias is longer (alias "App.DS.Sig" ends with user name "Sig") +// → user used a short / unqualified name in the command. +// 3. User name is longer (user name "App.GAM.In.Sig" ends with alias "GAM.In.Sig") +// → alias was registered with a short fallback path (FindByNameRecursive +// failed during broker init) but the client uses the full ORD path. +static bool AliasMatch(const char8 *aliasName, const char8 *userName) { + if (StringHelper::Compare(aliasName, userName) == 0) + return true; + if (SuffixMatch(aliasName, userName)) // case 2: alias ⊇ userName + return true; + if (SuffixMatch(userName, aliasName)) // case 3: userName ⊇ alias + return true; return false; } @@ -286,7 +304,21 @@ void DebugServiceBase::ProcessSignal(DebugSignalInfo *signalInfo, uint32 size, if (signalInfo == NULL_PTR(DebugSignalInfo *)) return; if (signalInfo->isForcing) { - memcpy(signalInfo->memoryAddress, signalInfo->forcedValue, size); + uint32 nEl = signalInfo->numberOfElements; + if (nEl <= 1u) { + // Scalar — single memcpy. + memcpy(signalInfo->memoryAddress, signalInfo->forcedValue, size); + } else { + // Array — copy only the elements whose bit is set in forcedMask. + uint32 elemBytes = size / nEl; + for (uint32 e = 0u; e < nEl; e++) { + if (signalInfo->forcedMask[e >> 3u] & (uint8)(1u << (e & 7u))) { + memcpy((uint8 *)signalInfo->memoryAddress + e * elemBytes, + signalInfo->forcedValue + e * elemBytes, + elemBytes); + } + } + } } if (signalInfo->isTracing) { Atomic::Add((volatile int32 *)&signalInfo->decimationCounter, 1); @@ -352,14 +384,39 @@ void DebugServiceBase::ConsumeStepIfNeeded(const char8 *gamName, // --------------------------------------------------------------------------- uint32 DebugServiceBase::ForceSignal(const char8 *name, const char8 *valueStr) { + // Parse optional array-element suffix: "SignalName[idx]" → baseName + elemIdx. + // elemIdx == -1 means "force all elements". + const char8 *bracketPos = StringHelper::SearchChar(name, '['); + StreamString baseName; + int32 elemIdx = -1; + + if (bracketPos != NULL_PTR(const char8 *)) { + uint32 baseLen = (uint32)(bracketPos - name); + (void)baseName.Write(name, baseLen); + const char8 *closePos = + StringHelper::SearchChar(bracketPos + 1u, ']'); + if (closePos != NULL_PTR(const char8 *)) { + uint32 idxLen = (uint32)(closePos - bracketPos - 1u); + StreamString idxStr; + (void)idxStr.Write(bracketPos + 1u, idxLen); + uint32 parsed = 0u; + AnyType iv(UnsignedInteger32Bit, 0u, &parsed); + AnyType is_src(CharString, 0u, idxStr.Buffer()); + if (TypeConvert(iv, is_src)) + elemIdx = (int32)parsed; + } + } else { + baseName = name; + } + const char8 *effectiveName = baseName.Buffer(); + mutex.FastLock(); uint32 count = 0u; for (uint32 i = 0u; i < aliases.GetNumberOfElements(); i++) { - if (aliases[i].name == name || - SuffixMatch(aliases[i].name.Buffer(), name)) { - DebugSignalInfo *s = signals[aliases[i].signalIndex]; - uint32 elemBytes = (uint32)(s->type.numberOfBits) / 8u; - uint32 totalBytes = elemBytes * s->numberOfElements; + if (AliasMatch(aliases[i].name.Buffer(), effectiveName)) { + DebugSignalInfo *s = signals[aliases[i].signalIndex]; + uint32 elemBytes = (uint32)(s->type.numberOfBits) / 8u; + uint32 totalBytes = elemBytes * s->numberOfElements; if (elemBytes == 0u || totalBytes > (uint32)sizeof(s->forcedValue)) { REPORT_ERROR_STATIC( ErrorManagement::Warning, @@ -369,9 +426,27 @@ uint32 DebugServiceBase::ForceSignal(const char8 *name, const char8 *valueStr) { continue; } s->isForcing = true; - AnyType dest(s->type, 0u, s->forcedValue); - AnyType source(CharString, 0u, valueStr); - (void)TypeConvert(dest, source); + + if (elemIdx >= 0 && (uint32)elemIdx < s->numberOfElements) { + // Force a single element — write to its slot and set its mask bit. + uint8 *dest = (uint8 *)s->forcedValue + (uint32)elemIdx * elemBytes; + AnyType dv(s->type, 0u, (void *)dest); + AnyType sv(CharString, 0u, valueStr); + (void)TypeConvert(dv, sv); + s->forcedMask[(uint32)elemIdx >> 3u] |= (uint8)(1u << ((uint32)elemIdx & 7u)); + } else { + // Force all elements to the same value — convert once, replicate, set all bits. + AnyType dv(s->type, 0u, s->forcedValue); + AnyType sv(CharString, 0u, valueStr); + (void)TypeConvert(dv, sv); + for (uint32 k = 1u; k < s->numberOfElements; k++) { + memcpy(s->forcedValue + k * elemBytes, s->forcedValue, elemBytes); + } + uint32 fullBytes = s->numberOfElements / 8u; + uint32 remBits = s->numberOfElements % 8u; + memset(s->forcedMask, 0xFF, fullBytes); + if (remBits > 0u) s->forcedMask[fullBytes] = (uint8)((1u << remBits) - 1u); + } count++; } } @@ -381,12 +456,48 @@ uint32 DebugServiceBase::ForceSignal(const char8 *name, const char8 *valueStr) { } uint32 DebugServiceBase::UnforceSignal(const char8 *name) { + // Parse optional "[idx]" suffix. If present, clear only that element's bit; + // if absent, clear all bits and the isForcing flag entirely. + const char8 *bracketPos = StringHelper::SearchChar(name, '['); + StreamString baseName; + int32 elemIdx = -1; + + if (bracketPos != NULL_PTR(const char8 *)) { + uint32 baseLen = (uint32)(bracketPos - name); + (void)baseName.Write(name, baseLen); + const char8 *closePos = StringHelper::SearchChar(bracketPos + 1u, ']'); + if (closePos != NULL_PTR(const char8 *)) { + uint32 idxLen = (uint32)(closePos - bracketPos - 1u); + StreamString idxStr; + (void)idxStr.Write(bracketPos + 1u, idxLen); + uint32 parsed = 0u; + AnyType iv(UnsignedInteger32Bit, 0u, &parsed); + AnyType is_src(CharString, 0u, idxStr.Buffer()); + if (TypeConvert(iv, is_src)) elemIdx = (int32)parsed; + } + } else { + baseName = name; + } + const char8 *effectiveName = baseName.Buffer(); + mutex.FastLock(); uint32 count = 0u; for (uint32 i = 0u; i < aliases.GetNumberOfElements(); i++) { - if (aliases[i].name == name || - SuffixMatch(aliases[i].name.Buffer(), name)) { - signals[aliases[i].signalIndex]->isForcing = false; + if (AliasMatch(aliases[i].name.Buffer(), effectiveName)) { + DebugSignalInfo *s = signals[aliases[i].signalIndex]; + if (elemIdx >= 0 && (uint32)elemIdx < s->numberOfElements) { + // Clear just this element's force bit. + s->forcedMask[(uint32)elemIdx >> 3u] &= (uint8)(~(1u << ((uint32)elemIdx & 7u))); + // Deactivate forcing entirely if no bits remain. + uint32 numBytes = (s->numberOfElements + 7u) / 8u; + bool anySet = false; + for (uint32 b = 0u; b < numBytes && !anySet; b++) anySet = (s->forcedMask[b] != 0u); + if (!anySet) s->isForcing = false; + } else { + // Unforce all elements. + memset(s->forcedMask, 0, sizeof(s->forcedMask)); + s->isForcing = false; + } count++; } } @@ -400,8 +511,7 @@ uint32 DebugServiceBase::TraceSignal(const char8 *name, bool enable, mutex.FastLock(); uint32 count = 0u; for (uint32 i = 0u; i < aliases.GetNumberOfElements(); i++) { - if (aliases[i].name == name || - SuffixMatch(aliases[i].name.Buffer(), name)) { + if (AliasMatch(aliases[i].name.Buffer(), name)) { DebugSignalInfo *s = signals[aliases[i].signalIndex]; s->isTracing = enable; s->decimationFactor = decimation; @@ -419,8 +529,7 @@ uint32 DebugServiceBase::SetBreak(const char8 *name, uint8 op, mutex.FastLock(); uint32 count = 0u; for (uint32 i = 0u; i < aliases.GetNumberOfElements(); i++) { - if (aliases[i].name == name || - SuffixMatch(aliases[i].name.Buffer(), name)) { + if (AliasMatch(aliases[i].name.Buffer(), name)) { DebugSignalInfo *s = signals[aliases[i].signalIndex]; s->breakThreshold = threshold; s->breakOp = op; @@ -437,8 +546,7 @@ uint32 DebugServiceBase::ClearBreak(const char8 *name) { mutex.FastLock(); uint32 count = 0u; for (uint32 i = 0u; i < aliases.GetNumberOfElements(); i++) { - if (aliases[i].name == name || - SuffixMatch(aliases[i].name.Buffer(), name)) { + if (AliasMatch(aliases[i].name.Buffer(), name)) { signals[aliases[i].signalIndex]->breakOp = BREAK_OFF; count++; } @@ -454,8 +562,7 @@ bool DebugServiceBase::IsInstrumented(const char8 *fullPath, bool &traceable, mutex.FastLock(); bool found = false; for (uint32 i = 0u; i < aliases.GetNumberOfElements(); i++) { - if (aliases[i].name == fullPath || - SuffixMatch(aliases[i].name.Buffer(), fullPath)) { + if (AliasMatch(aliases[i].name.Buffer(), fullPath)) { found = true; break; } @@ -517,8 +624,7 @@ uint32 DebugServiceBase::RegisterMonitorSignal(const char8 *path, m.internalID = 0x80000000u | monitoredSignals.GetNumberOfElements(); // Re-use existing brokered signal ID if available for (uint32 i = 0u; i < aliases.GetNumberOfElements(); i++) { - if (aliases[i].name == path || - SuffixMatch(aliases[i].name.Buffer(), path)) { + if (AliasMatch(aliases[i].name.Buffer(), path)) { m.internalID = signals[aliases[i].signalIndex]->internalID; break; } @@ -652,8 +758,7 @@ void DebugServiceBase::GetSignalValue(const char8 *name, StreamString &out) { mutex.FastLock(); DebugSignalInfo *sig = NULL_PTR(DebugSignalInfo *); for (uint32 i = 0u; i < aliases.GetNumberOfElements(); i++) { - if (aliases[i].name == name || - SuffixMatch(aliases[i].name.Buffer(), name)) { + if (AliasMatch(aliases[i].name.Buffer(), name)) { sig = signals[aliases[i].signalIndex]; break; } @@ -809,14 +914,156 @@ void DebugServiceBase::BuildDiscoverCache() { } void DebugServiceBase::BuildTreeCache() { - treeCacheValid = false; - treeCache = ""; - treeCache += "{\"Name\":\"Root\",\"Class\":\"ObjectRegistryDatabase\"," - "\"Children\":[\n"; - (void)ExportTree(ObjectRegistryDatabase::Instance(), treeCache, - NULL_PTR(const char8 *)); - treeCache += "\n]}\nOK TREE\n"; - treeCacheValid = true; + // No-op: tree is now served per-node via ExportTreeNode. +} + +// --------------------------------------------------------------------------- +// ExportTreeNode — serve one level of the object tree +// --------------------------------------------------------------------------- + +void DebugServiceBase::ExportTreeNode(const char8 *path, StreamString &out) { + bool isRoot = + (path == NULL_PTR(const char8 *) || StringHelper::Length(path) == 0); + + out += "{\"Path\":\""; + EscapeJson(isRoot ? "" : path, out); + out += "\",\"Children\":[\n"; + + uint32 count = 0u; + + ReferenceContainer *container = NULL_PTR(ReferenceContainer *); + DataSourceI * ds = NULL_PTR(DataSourceI *); + GAM * gam = NULL_PTR(GAM *); + + if (isRoot) { + container = ObjectRegistryDatabase::Instance(); + } else { + Reference ref = ObjectRegistryDatabase::Instance()->Find(path); + if (ref.IsValid()) { + container = dynamic_cast(ref.operator->()); + ds = dynamic_cast(ref.operator->()); + gam = dynamic_cast(ref.operator->()); + } + } + + // ReferenceContainer children + if (container != NULL_PTR(ReferenceContainer *)) { + uint32 n = container->Size(); + for (uint32 i = 0u; i < n; i++) { + Reference child = container->Get(i); + if (!child.IsValid()) + continue; + const char8 *cname = child->GetName(); + if (cname == NULL_PTR(const char8 *)) + cname = "unnamed"; + + ReferenceContainer *irc = + dynamic_cast(child.operator->()); + DataSourceI *ids = dynamic_cast(child.operator->()); + GAM * igam = dynamic_cast(child.operator->()); + bool hasCh = + (irc != NULL_PTR(ReferenceContainer *) && irc->Size() > 0u) || + ids != NULL_PTR(DataSourceI *) || igam != NULL_PTR(GAM *); + + if (count > 0u) + out += ",\n"; + out += "{\"Name\":\""; + EscapeJson(cname, out); + out += "\",\"Class\":\""; + EscapeJson(child->GetClassProperties()->GetName(), out); + out.Printf("\",\"HasChildren\":%s}", hasCh ? "true" : "false"); + count++; + } + } + + // DataSourceI signals + if (ds != NULL_PTR(DataSourceI *)) { + uint32 ns = ds->GetNumberOfSignals(); + for (uint32 j = 0u; j < ns; j++) { + StreamString sn; + (void)ds->GetSignalName(j, sn); + const char8 *st = TypeDescriptor::GetTypeNameFromTypeDescriptor( + ds->GetSignalType(j)); + uint8 d = 0u; + (void)ds->GetSignalNumberOfDimensions(j, d); + uint32 el = 0u; + (void)ds->GetSignalNumberOfElements(j, el); + StreamString sfp; + sfp.Printf("%s.%s", path, sn.Buffer()); + bool tr = false, fo = false; + (void)IsInstrumented(sfp.Buffer(), tr, fo); + + if (count > 0u) + out += ",\n"; + out += "{\"Name\":\""; + EscapeJson(sn.Buffer(), out); + out += "\",\"Class\":\"Signal\",\"Type\":\""; + EscapeJson(st ? st : "Unknown", out); + out.Printf("\",\"Dimensions\":%u,\"Elements\":%u," + "\"IsTraceable\":%s,\"IsForcable\":%s,\"HasChildren\":false}", + d, el, tr ? "true" : "false", fo ? "true" : "false"); + count++; + } + } + + // GAM input signals + if (gam != NULL_PTR(GAM *)) { + uint32 nIn = gam->GetNumberOfInputSignals(); + for (uint32 j = 0u; j < nIn; j++) { + StreamString sn; + (void)gam->GetSignalName(InputSignals, j, sn); + const char8 *st = TypeDescriptor::GetTypeNameFromTypeDescriptor( + gam->GetSignalType(InputSignals, j)); + uint32 d = 0u; + (void)gam->GetSignalNumberOfDimensions(InputSignals, j, d); + uint32 el = 0u; + (void)gam->GetSignalNumberOfElements(InputSignals, j, el); + StreamString sfp; + sfp.Printf("%s.In.%s", path, sn.Buffer()); + bool tr = false, fo = false; + (void)IsInstrumented(sfp.Buffer(), tr, fo); + + if (count > 0u) + out += ",\n"; + out += "{\"Name\":\"In."; + EscapeJson(sn.Buffer(), out); + out += "\",\"Class\":\"InputSignal\",\"Type\":\""; + EscapeJson(st ? st : "Unknown", out); + out.Printf("\",\"Dimensions\":%u,\"Elements\":%u," + "\"IsTraceable\":%s,\"IsForcable\":%s,\"HasChildren\":false}", + d, el, tr ? "true" : "false", fo ? "true" : "false"); + count++; + } + + uint32 nOut = gam->GetNumberOfOutputSignals(); + for (uint32 j = 0u; j < nOut; j++) { + StreamString sn; + (void)gam->GetSignalName(OutputSignals, j, sn); + const char8 *st = TypeDescriptor::GetTypeNameFromTypeDescriptor( + gam->GetSignalType(OutputSignals, j)); + uint32 d = 0u; + (void)gam->GetSignalNumberOfDimensions(OutputSignals, j, d); + uint32 el = 0u; + (void)gam->GetSignalNumberOfElements(OutputSignals, j, el); + StreamString sfp; + sfp.Printf("%s.Out.%s", path, sn.Buffer()); + bool tr = false, fo = false; + (void)IsInstrumented(sfp.Buffer(), tr, fo); + + if (count > 0u) + out += ",\n"; + out += "{\"Name\":\"Out."; + EscapeJson(sn.Buffer(), out); + out += "\",\"Class\":\"OutputSignal\",\"Type\":\""; + EscapeJson(st ? st : "Unknown", out); + out.Printf("\",\"Dimensions\":%u,\"Elements\":%u," + "\"IsTraceable\":%s,\"IsForcable\":%s,\"HasChildren\":false}", + d, el, tr ? "true" : "false", fo ? "true" : "false"); + count++; + } + } + + out += "\n]}\nOK TREE\n"; } void DebugServiceBase::InfoNode(const char8 *path, StreamString &out) { @@ -857,8 +1104,7 @@ void DebugServiceBase::InfoNode(const char8 *path, StreamString &out) { mutex.FastLock(); bool found = false; for (uint32 i = 0u; i < aliases.GetNumberOfElements(); i++) { - if (aliases[i].name == path || - SuffixMatch(aliases[i].name.Buffer(), path)) { + if (AliasMatch(aliases[i].name.Buffer(), path)) { DebugSignalInfo *s = signals[aliases[i].signalIndex]; const char8 *tn = TypeDescriptor::GetTypeNameFromTypeDescriptor(s->type); @@ -922,7 +1168,6 @@ void DebugServiceBase::SetFullConfig(ConfigurationDatabase &config) { config.Copy(fullConfig); manualConfigSet = true; BuildDiscoverCache(); - BuildTreeCache(); } void DebugServiceBase::RebuildConfigFromRegistry() { @@ -1289,9 +1534,10 @@ void DebugServiceBase::HandleCommand(const StreamString &cmdIn, } else if (token == "DISCOVER") { Discover(out); } else if (token == "TREE") { - if (!treeCacheValid) - BuildTreeCache(); - out += treeCache; + StreamString path; + (void)cmd.GetToken(path, delims, term); + ExportTreeNode(path.Size() > 0u ? path.Buffer() : NULL_PTR(const char8 *), + out); } else if (token == "INFO") { StreamString path; if (cmd.GetToken(path, delims, term)) diff --git a/Source/Components/Interfaces/DebugService/DebugServiceBase.h b/Source/Components/Interfaces/DebugService/DebugServiceBase.h index 6f08b9f..3b1dd2d 100644 --- a/Source/Components/Interfaces/DebugService/DebugServiceBase.h +++ b/Source/Components/Interfaces/DebugService/DebugServiceBase.h @@ -176,6 +176,7 @@ protected: uint32 ExportTree(ReferenceContainer *container, StreamString &json, const char8 *pathPrefix); + void ExportTreeNode(const char8 *path, StreamString &out); void EnrichWithConfig(const char8 *path, StreamString &json); static void JsonifyDatabase(ConfigurationDatabase &db, StreamString &json); diff --git a/Tools/go_web_client/marte2-web-client b/Tools/go_web_client/marte2-web-client index 0ac5c7f..203c4b3 100755 Binary files a/Tools/go_web_client/marte2-web-client and b/Tools/go_web_client/marte2-web-client differ diff --git a/Tools/go_web_client/marte2.go b/Tools/go_web_client/marte2.go index eab5229..aa50ddf 100644 --- a/Tools/go_web_client/marte2.go +++ b/Tools/go_web_client/marte2.go @@ -347,9 +347,11 @@ func (m *MarteClient) handleJSONResponse(tag, data string) { return case "TREE": - // Parse server-side and stream pre-digested flat-node batches to the - // browser so it never has to JSON.parse a multi-MB string. - go m.sendTreeBatches(data) + // Per-node response — forward to browser as-is. + broadcast(m.hub, map[string]any{ + "type": "tree_node", + "data": data, + }) return } broadcast(m.hub, map[string]any{ @@ -359,90 +361,6 @@ func (m *MarteClient) handleJSONResponse(tag, data string) { }) } -// --------------------------------------------------------------------------- -// TREE streaming -// --------------------------------------------------------------------------- - -// TreeNode mirrors the DebugService TREE JSON structure. -type TreeNode struct { - Name string `json:"Name"` - Class string `json:"Class"` - IsTraceable bool `json:"IsTraceable"` - IsForcable bool `json:"IsForcable"` - Elements uint32 `json:"Elements"` - Children []*TreeNode `json:"Children"` -} - -// FlatNode is a pre-digested, compact representation sent to the browser. -type FlatNode struct { - Path string `json:"p"` - Name string `json:"n"` - Class string `json:"c"` - Parent string `json:"par"` - Tr bool `json:"tr,omitempty"` - Fo bool `json:"fo,omitempty"` - El uint32 `json:"el,omitempty"` - HasCh bool `json:"ch,omitempty"` -} - -func flattenTree(node *TreeNode, parentPath string, out *[]FlatNode) { - path := node.Name - if parentPath != "" { - path = parentPath + "." + node.Name - } - hasCh := len(node.Children) > 0 - *out = append(*out, FlatNode{ - Path: path, - Name: node.Name, - Class: node.Class, - Parent: parentPath, - Tr: node.IsTraceable, - Fo: node.IsForcable, - El: node.Elements, - HasCh: hasCh, - }) - for _, c := range node.Children { - flattenTree(c, path, out) - } -} - -// sendTreeBatches parses the raw TREE JSON and streams compact FlatNode -// batches to all browsers. Each browser processes one batch per animation -// frame, keeping the UI responsive during large tree loads. -func (m *MarteClient) sendTreeBatches(raw string) { - var root TreeNode - if err := json.Unmarshal([]byte(raw), &root); err != nil { - log.Printf("[TREE] parse error: %v — falling back to raw JSON", err) - broadcast(m.hub, map[string]any{"type": "response", "tag": "TREE", "data": raw}) - return - } - - var nodes []FlatNode - if root.Name == "Root" && len(root.Children) > 0 { - for _, c := range root.Children { - flattenTree(c, "", &nodes) - } - } else { - flattenTree(&root, "", &nodes) - } - - total := len(nodes) - log.Printf("[TREE] streaming %d flat nodes to browser", total) - broadcast(m.hub, map[string]any{"type": "tree_clear", "total": total}) - - const batchSize = 50 - for i := 0; i < total; i += batchSize { - end := i + batchSize - if end > total { - end = total - } - broadcast(m.hub, map[string]any{ - "type": "tree_batch", - "nodes": nodes[i:end], - "done": end == total, - }) - } -} func (m *MarteClient) handleTextLine(line string) { if strings.HasPrefix(line, "OK SERVICE_INFO") { diff --git a/Tools/go_web_client/static/app.js b/Tools/go_web_client/static/app.js index 9d028c9..7f4fd01 100644 --- a/Tools/go_web_client/static/app.js +++ b/Tools/go_web_client/static/app.js @@ -5,7 +5,7 @@ // State // ════════════════════════════════════════════════════════════════════ -const MAX_TRACE_PTS = 100000; +const MAX_TRACE_PTS = 500000; // per-signal storage limit; LTTB downsamples for display const COLORS = [ '#89b4fa','#f38ba8','#a6e3a1','#fab387','#f9e2af', '#cba6f7','#89dceb','#b4befe','#eba0ac','#94e2d5' @@ -23,8 +23,10 @@ let signalsByName = {}; let traceData = {}; // Set of names currently being traced let tracedSet = new Set(); -// Forced: name → value string +// Forced: name → value string (scalars and single-index array elements) let forcedSignals = {}; +// Forced array groups: baseName → {isAll, indices: Set, elements, value, expanded} +const forcedGroups = new Map(); // Breakpoints: name → {op, threshold} let breakpoints = {}; // Thread list from TREE @@ -51,6 +53,19 @@ let unforceTarget = ''; // UDP stats let udpPackets = 0, udpDropped = 0; +// Grouped array traces: baseName → {indices: Set, elements: number, expanded: boolean} +// Created when user traces All/Range from the array dialog. +const tracedGroups = new Map(); + +// Sequential array series: baseName → {seqName, nEl, lastTs} +const seqState = {}; +// Waterfall panels: wfId → wfObj +const waterfalls = new Map(); +let wfSeq = 0; +let wfRO = null; +// Pending array-plot dialog state +let _arrpPlotId = -1, _arrpName = '', _arrpElem = 0; + // ════════════════════════════════════════════════════════════════════ // WebSocket // ════════════════════════════════════════════════════════════════════ @@ -84,59 +99,113 @@ function handleMsg(msg) { case 'log': onLog(msg); break; case 'service_config': onServiceConfig(msg); break; case 'udp_stats': onUdpStats(msg); break; - case 'tree_clear': onTreeClear(msg.total); break; - case 'tree_batch': onTreeBatch(msg); break; + case 'tree_node': try { onTreeNode(JSON.parse(msg.data)); } catch(e) {} break; } } // ════════════════════════════════════════════════════════════════════ -// Incremental tree rendering (server-side pre-processed batches) +// Per-node lazy tree loading // -// Batches are stored purely in memory — no DOM work during receipt. -// DOM nodes are created lazily: only the top-level containers are -// built immediately; everything inside a closed
is deferred -// until the user opens it. This keeps the initial render O(1) in the -// number of signals regardless of tree size. +// TREE (no arg) returns only the root-level children. +// TREE returns the immediate children of that node. +// Each
element sends TREE on first open to load +// its children on demand, keeping initial render O(1). // ════════════════════════════════════════════════════════════════════ -let _treeBuild = null; // { threads:[], objects:[] } — null when idle -let _flatNodes = null; // Map -let _childrenOf = null; // Map +// Called when a tree_node message arrives from the Go server. +function onTreeNode(data) { + const path = data.Path || ''; + const children = data.Children || []; -function onTreeClear(total) { - _treeBuild = { threads: [], objects: [] }; - _flatNodes = new Map(); - _childrenOf = new Map(); - document.getElementById('tree-body').innerHTML = ''; - appendLog('sys', 'INFO', `Receiving tree (${total} nodes)…`); + if (path === '') { + // Root level: populate tree-body and mark tree ready. + const body = document.getElementById('tree-body'); + body.innerHTML = ''; + const frag = document.createDocumentFragment(); + for (const child of children) { + frag.appendChild(_buildLazyNode(child, '')); + } + body.appendChild(frag); + _treeReady = true; + appendLog('sys', 'INFO', `Tree root loaded — ${children.length} top-level nodes`); + startStepPoll(); + } else { + // Find the
element for this path and populate it. + const details = document.querySelector(`[data-tree-path="${CSS.escape(path)}"]`); + if (details) { + // Remove loading indicator. + const ld = details.querySelector('.tree-loading'); + if (ld) ld.remove(); + const childDiv = document.createElement('div'); + childDiv.className = 'children'; + const infoBtn = makeLeafButtons(path); + if (infoBtn) childDiv.appendChild(infoBtn); + const frag = document.createDocumentFragment(); + for (const child of children) { + frag.appendChild(_buildLazyNode(child, path)); + } + childDiv.appendChild(frag); + details.appendChild(childDiv); + } + } + + // Collect metadata (threads, objects) from the received children. + for (const child of children) { + const fullPath = path ? path + '.' + child.Name : child.Name; + if (child.Class === 'RealTimeThread' && !threads.includes(child.Name)) { + threads.push(child.Name); + } + if (child.HasChildren) { + allObjects.push(fullPath); + } + } + _updateTreeMeta(); } -function onTreeBatch(msg) { - if (!_treeBuild) return; - // Pure memory work — no DOM touched here. - for (const node of (msg.nodes || [])) { - _flatNodes.set(node.p, node); - const pk = node.par || ''; - let ch = _childrenOf.get(pk); - if (!ch) { ch = []; _childrenOf.set(pk, ch); } - ch.push(node); - if (node.c === 'RealTimeThread' && node.n) _treeBuild.threads.push(node.n); - if (node.c && node.c !== 'Signal' && node.n) _treeBuild.objects.push(node.p); +// Build one lazy tree node DOM element. +// Containers get a
that fires TREE on first open. +// Leaves are rendered immediately as signal rows. +function _buildLazyNode(child, parentPath) { + const fullPath = parentPath ? parentPath + '.' + child.Name : child.Name; + + if (child.HasChildren) { + const details = document.createElement('details'); + details.setAttribute('data-tree-path', fullPath); + const summary = document.createElement('summary'); + summary.innerHTML = + `${esc(child.Name)}` + + `[${esc(child.Class)}]`; + details.appendChild(summary); + details.addEventListener('toggle', function() { + if (!details.open || details._childrenBuilt) return; + details._childrenBuilt = true; + const ld = document.createElement('div'); + ld.className = 'tree-loading'; + ld.textContent = '⏳ Loading…'; + details.appendChild(ld); + sendCmd('TREE ' + fullPath); + }); + return details; } - if (!msg.done) return; + // Leaf node (signal). + const isSig = child.Class === 'Signal' || + child.Class === 'InputSignal' || + child.Class === 'OutputSignal' || + child.IsTraceable; + const item = { + Name: child.Name, Class: child.Class, + IsTraceable: child.IsTraceable, IsForcable: child.IsForcable, + Elements: child.Elements || 1 + }; + const row = document.createElement('div'); + row.className = 'tree-node'; + row.appendChild(makeLeafRow(fullPath, item, isSig)); + return row; +} - // All batches received — build only top-level nodes now. - const body = document.getElementById('tree-body'); - const frag = document.createDocumentFragment(); - for (const node of (_childrenOf.get('') || [])) { - frag.appendChild(_buildFlatNode(node)); - } - body.appendChild(frag); - - // Finalise autocompletes. - threads = _treeBuild.threads; - allObjects = _treeBuild.objects; +// Update thread selects and object autocomplete from current state. +function _updateTreeMeta() { ['step-thread','step-thread-list','step-thread-inp'].forEach(id => { const el = document.getElementById(id); if (!el) return; @@ -149,68 +218,6 @@ function onTreeBatch(msg) { }); document.getElementById('msg-dest-list').innerHTML = allObjects.map(n => `