Skip to content

Commit 4924249

Browse files
perf(traceTransaction): embed trace via string concatenation
The trace file is JSONL already produced by the semantics, so decoding each record with String2JSON and re-encoding the whole array via JSON2String was a redundant round-trip whose cost scales with the trace (which can be very large). Trust the file and build the result array by concatenating the lines with commas inside brackets, running only the small id through the JSON encoder.
1 parent 525fa64 commit 4924249

2 files changed

Lines changed: 47 additions & 20 deletions

File tree

docs/node-semantics.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ The trace is not part of the receipt — the executing steps already appended it
105105

106106
### traceTransaction
107107

108-
`traceTransaction` is a komet-specific extension, not part of the Stellar RPC specification (see [server.md](server.md#tracetransaction-komet-specific-extension)). It is a read-only lookup: it takes a `hash` (the same parameter `getTransaction` takes) and responds with the contents of `traces/trace_<hash>.jsonl` parsed into a JSON array (one record per executed instruction), or `null` when no trace file exists for that hash. Because tracing is always on, every `sendTransaction` writes this file.
108+
`traceTransaction` is a komet-specific extension, not part of the Stellar RPC specification (see [server.md](server.md#tracetransaction-komet-specific-extension)). It is a read-only lookup: it takes a `hash` (the same parameter `getTransaction` takes) and responds with the contents of `traces/trace_<hash>.jsonl` as a JSON array (one record per executed instruction), or `null` when no trace file exists for that hash. Rather than decoding and re-encoding each record — a full round-trip whose cost scales with the (potentially very large) trace — the semantics trust that the file the tracer wrote is already valid JSON and build the array by string concatenation, joining the lines with commas inside `[``]`. Because tracing is always on, every `sendTransaction` writes this file.
109109

110110
### Two ways steps are delivered
111111

src/komet_node/kdist/node.md

Lines changed: 46 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -527,47 +527,74 @@ the spec-mandated `resultXdr`/`resultMetaXdr` base64 XDR fields (K cannot constr
527527

528528
Retrieve the execution trace of a previously submitted transaction, looked up by `hash` (the
529529
same parameter `getTransaction` takes). The trace was written to `traces/trace_<hash>.jsonl`
530-
by `sendTransaction`. The file is JSONL (one JSON record per executed instruction); we parse
531-
it into a JSON array so the result is structured data rather than an opaque string. Responds
532-
with that array — empty when the transaction ran no instructions — or `null` when no trace
533-
file exists for that hash.
530+
by `sendTransaction`. The file is JSONL (one JSON record per executed instruction), and each
531+
line is already valid JSON produced by the semantics — so rather than decoding every record
532+
into a `JSON` term and re-encoding the whole array (a full round-trip whose cost scales with
533+
the trace, which can be very large), we build the result array by *string concatenation*:
534+
join the trusted lines with commas and wrap them in `[``]`. Responds with that array —
535+
empty when the transaction ran no instructions — or `null` when no trace file exists for that
536+
hash.
537+
538+
Because the trace body is spliced in as raw text, `#respondTrace` writes the JSON-RPC
539+
envelope directly (mirroring `#respond`) instead of building a `JSON` term for `JSON2String`:
540+
only the small `id` value is serialized through the encoder.
534541

535542
```k
536543
rule <k> #dispatchMethod( "traceTransaction", REQ )
537544
=> #respondTrace( #getJSON( "id", REQ ), #getString( "hash", REQ ) )
538545
...
539546
</k>
540547
541-
rule <k> #respondTrace( ID, HASH ) => #respond( ID, [ #parseTraceLines( {#readFile( #traceFile( HASH ) )}:>String ) ] ) ... </k>
548+
rule <k> #respondTrace( ID, HASH )
549+
=> #writeFile( "response.json",
550+
"{\"jsonrpc\":\"2.0\",\"id\":"
551+
+String JSON2String( ID )
552+
+String ",\"result\":"
553+
+String #traceArray( {#readFile( #traceFile( HASH ) )}:>String )
554+
+String "}" )
555+
~> #remove( "request.json" )
556+
...
557+
</k>
558+
<exitCode> _ => 0 </exitCode>
542559
requires #fileExists( #traceFile( HASH ) )
560+
543561
rule <k> #respondTrace( ID, HASH ) => #respond( ID, null ) ... </k>
544562
requires notBool #fileExists( #traceFile( HASH ) )
545563
```
546564

547-
`#parseTraceLines` turns the JSONL trace text into a `JSONs` list, parsing each newline-
548-
delimited record with `String2JSON`. Empty segments (a leading/blank line, or the empty
549-
tail after the final record's trailing newline) are skipped, so an empty file yields `.JSONs`
550-
(an empty array).
565+
`#traceArray` wraps the JSONL trace text in array brackets; `#traceElems` joins the
566+
newline-delimited records with commas without touching their contents. Empty segments (a
567+
leading/blank line, or the empty tail after the final record's trailing newline) are skipped
568+
via `#maybeComma`, which prefixes a separator only when a following record actually exists —
569+
so a trailing newline never yields a dangling comma, and an empty file yields `[]`.
551570

552571
```k
553-
syntax JSONs ::= #parseTraceLines( String ) [function, symbol(parseTraceLines)]
554-
// -------------------------------------------------------------------------------
555-
rule #parseTraceLines( "" ) => .JSONs
572+
syntax String ::= #traceArray( String ) [function, symbol(traceArray)]
573+
| #traceElems( String ) [function, symbol(traceElems)]
574+
| #maybeComma( String ) [function, symbol(maybeComma)]
575+
// -----------------------------------------------------------------------
576+
rule #traceArray( S ) => "[" +String #traceElems( S ) +String "]"
577+
578+
rule #traceElems( "" ) => ""
556579
557580
// No more newlines: the whole remaining string is the final record.
558-
rule #parseTraceLines( S ) => String2JSON( S ) , .JSONs
581+
rule #traceElems( S ) => S
559582
requires S =/=String "" andBool findString( S, "\n", 0 ) <Int 0
560583
561-
// Split off the first line and recurse on the rest.
562-
rule #parseTraceLines( S )
563-
=> String2JSON( substrString( S, 0, findString( S, "\n", 0 ) ) )
564-
, #parseTraceLines( substrString( S, findString( S, "\n", 0 ) +Int 1, lengthString( S ) ) )
584+
// Split off the first line and recurse; a comma is added only if the tail is non-empty.
585+
rule #traceElems( S )
586+
=> substrString( S, 0, findString( S, "\n", 0 ) )
587+
+String #maybeComma( #traceElems( substrString( S, findString( S, "\n", 0 ) +Int 1, lengthString( S ) ) ) )
565588
requires findString( S, "\n", 0 ) >Int 0
566589
567590
// Empty leading line (the string starts with a newline): drop it and recurse.
568-
rule #parseTraceLines( S )
569-
=> #parseTraceLines( substrString( S, 1, lengthString( S ) ) )
591+
rule #traceElems( S )
592+
=> #traceElems( substrString( S, 1, lengthString( S ) ) )
570593
requires findString( S, "\n", 0 ) ==Int 0
594+
595+
rule #maybeComma( "" ) => ""
596+
rule #maybeComma( S ) => "," +String S
597+
requires S =/=String ""
571598
```
572599

573600
## simulateTransaction

0 commit comments

Comments
 (0)