perf: 2x journal replay throughput via parser optimizations + per-phase PM#4980
Open
abdelaziz-mahdy wants to merge 5 commits intodevelopmentfrom
Open
perf: 2x journal replay throughput via parser optimizations + per-phase PM#4980abdelaziz-mahdy wants to merge 5 commits intodevelopmentfrom
abdelaziz-mahdy wants to merge 5 commits intodevelopmentfrom
Conversation
Consolidated Java test exercising DoubleParser, StringParser, and the JSON roundtrip path through FObjectParser + ModelParserFactory. Captures the pre-optimization behavior of: - DoubleParser on scientific notation, leading zeros, negative zero sign bit, Double.MAX_VALUE boundary, irrational-decimal ULP bound, and negative decimals. - StringParser on empty strings, escape sequences, unicode escapes, raw non-ASCII, and the three delimiter styles. - FObjectParser+ModelParserFactory on zero / leading-trailing outer / LF-CR-tab-between-body whitespace, single- and multi-property models, adjacent-comma rejection, and missing-comma-between-body-properties rejection. All 23 cases pass against the unchanged parsers on upstream/development, establishing a baseline that subsequent parser-optimization cherry-picks must preserve.
Four independent optimizations to the FOAM combinator parser,
each benchmarked independently against 1M journal entries (981 MB):
1. Inline SkipParser (ModelParserFactory.java)
Replace 5-layer combinator stack (Repeat0→Alt→Seq0→Literal→WS)
with direct char checks in a single while-loop.
Result: +24.8% (37.53s → 28.24s)
2. CharLiteral specialization (new CharLiteral.java + AbstractLiteral.java)
Single-char literals ({, }, :, ,) skip the loop and String.charAt
in AbstractLiteral.parse(). Factory auto-selects for length==1.
Result: +2.6% (28.24s → 27.51s)
3. Remove String.intern() (StringParser.java)
intern() hits the JVM global native string table with C++ sync
~30M times. Most journal strings are unique — pure overhead.
Result: +25.1% (27.51s → 21.99s)
4. Arithmetic DoubleParser (DoubleParser.java)
Replace StringBuilder + Double.valueOf(sb.toString()) with direct
long arithmetic accumulation. Zero heap allocation per number.
Result: +14.4% (21.99s → 18.82s)
Also tested and REJECTED:
- PrefixAlt flat char[] dispatch: -12.3% regression (Character.compare
is already JIT-optimized as an intrinsic)
Cumulative: 37.53s → 18.82s (49.9% faster, ~2x throughput)
FOAM gap to Jackson narrowed from 4.8x to ~2.4x
Tests verified:
- GrammarCombinatorsJavaTest: 81/81 passed
- F3FileJournalTest: 32/32 passed
- JournalReplayBenchmark: all 8 assertions passed
StringParser previously called ps.apply(delimiter, x) on every character
to check for the closing quote — a Literal.parse() virtual dispatch per
char. For double-quoted strings without escape sequences (95%+ of journal
strings), now uses String.indexOf() to find the closing quote in one
native call, then extracts the substring directly.
Follows the same pattern as UntilLiteral (indexOf on StringPStream) but
adds a backslash pre-check: if indexOf('\\') finds an escape before the
closing quote, falls back to the existing char-by-char loop.
Added getString() and createAt() to StringPStream for cross-package
access to the underlying string and position-based construction.
Result: 20.94s → 17.45s (+16.7%), FOAM gap to Jackson: 2.0x
…Factory
Replace Seq0(SKIP, Literal(':'), SKIP, valueParser) per property with a
single parser that does direct char checks: inline whitespace skip,
direct ':' comparison, inline whitespace skip, then value parse + set.
Eliminates 4 combinator layers (Seq0 dispatch) x 50 properties x 1M
entries = 200M virtual dispatch calls removed.
Also replace the outer Repeat0(Seq0(SKIP, Alt, SKIP), Literal(',')) with
an inlined property loop: direct whitespace skip, PrefixAlt dispatch,
direct ',' char check. Eliminates Repeat0 + Seq0 + Literal overhead.
Result: 17.45s → 15.53s (+11.0%), FOAM gap to Jackson: 1.7x
Break journal replay cost into four accumulators — getEntry, parse, findMerge, daoWrite — and emit one PM row per phase at replay completion. Gives downstream observability of where each DAO's replay time is actually going without another run. Adds operation counters (opCreate / opPut / opPutMerged / opRemove) and a commentsSkipped counter to the replay-complete log line, useful for reasoning about journal shape across environments. Replaces the COMMENT regex check with a `charAt(0) == '/'` fast path. Every comment in the existing comment regex starts with '/', and data entries always start with 'c', 'p', 'r', or 'v', so the predicate is superset-safe for the regex and saves a regex match on every data line. Covers both F3FileJournal (assembly-line replay) and FileJournal (single-threaded replay). F3FileJournal keeps DOS-3126's parseX context derivation and DOS-3191's OP_CREATE fall-through into OP_PUT intact.
kgrgreer
reviewed
Apr 17, 2026
| // Fast comment check: every comment starts with '/', which is | ||
| // never the first char of a data entry ('c', 'p', 'r', 'v'). | ||
| // Superset-safe for the block/line comment regex in AbstractF3FileJournal. | ||
| if ( entry.charAt(0) == '/' ) { commentsSkipped++; continue; } |
Owner
There was a problem hiding this comment.
Does this still work with multi-line comments?
Owner
There was a problem hiding this comment.
I'm not sure the old one supports multi-line comments, despite the REGEX supporting them.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Journal replay at boot dominates startup time on deployments with large data journals. Profiling a 1M-entry replay on a 981 MB journal shows parse consuming 90%+ of wall time, with I/O, find/merge, and DAO write each under 10%. The FOAM combinator parser pays per-character virtual dispatch through
Parser.parse()and allocates aStringBuilder, several intermediate combinator objects, and aString.intern()lookup per token — overhead that dominates on the hot path. No per-phase timing exists, so it was impossible to confirm where replay time was actually going.Solution
Optimize the four hot paths inside
foam.lib.jsonandfoam.lib.parse, keeping the parser surface identical. Add a consolidated regression test against those paths. Add per-phase PM instrumentation for journal replay so future profiling has ground truth without re-running with a profiler attached.Parser optimizations (2x replay throughput):
DoubleParser— replaceStringBuilder + Double.valueOfwith direct long arithmetic. Zero heap allocation per number; preserves IEEE-754 negative-zero sign bit; stays within one ULP ofDouble.valueOfon irrational decimals.StringParser— dropString.intern()(every parsed string hit the JVM's global intern table under C++ synchronization) and add anindexOf(delimiter)fast path for strings with no escapes (~95%+ of journal entries). The fast path falls back to the slow char-by-char loop when a backslash is seen before the closing delimiter.ModelParserFactory— inline the whitespace-skip, the property-value parser, and the comma separator. Eliminates a 5-layer combinator stack (Repeat0→Alt→Seq0→Literal→WS) and ~200M virtual-dispatch calls per 1M-entry replay.CharLiteral— single-char literals ({,},:,,) bypass the genericAbstractLiteral.parse()loop and itsString.charAt.AbstractLiteral.create("c")now returns aCharLiteralfor any one-char input.Regression tests (
ParserOptimizationTest): 23 cases locking in pre-optimization behavior — scientific notation, leading zeros, negative-zero sign bit,Double.MAX_VALUEboundary, irrational-decimal ULP bound, escape sequences, unicode escapes, the three string delimiter styles, and JSON roundtrip throughFObjectParser+ModelParserFactorycovering zero / leading-trailing outer / LF-CR-tab-between-body whitespace, single- and multi-property models, adjacent-comma rejection, and missing-comma-between-body-properties rejection. All 23 pass against the unchanged parsers and against each optimization commit — failures after cherry-pick pinpoint the regressing optimization.Per-phase PM instrumentation:
F3FileJournalandFileJournalnow accumulate four timing buckets (getEntry / parse / findMerge / daoWrite) and emit one PM row per phase at replay completion. The replay-complete log line addsopCreate/opPut/opPutMerged/opRemove/commentsSkippedcounters. Comment check switches from aPattern.matcher(entry).matches()regex call toentry.charAt(0) == '/'— superset-safe for the existing comment regex since every comment starts with/and data entries start withc/p/r/v.Changes
Parser
src/foam/lib/json/DoubleParser.java— arithmetic accumulation, noStringBuilder, noDouble.valueOfsrc/foam/lib/json/StringParser.java— dropintern(), addindexOffast pathsrc/foam/lib/json/ModelParserFactory.java— inlineSKIP, inline property+comma, inline value parsersrc/foam/lib/parse/AbstractLiteral.java— factory returnsCharLiteralfor single-char inputssrc/foam/lib/parse/CharLiteral.java— new, specialized single-char literal parsersrc/foam/lib/parse/StringPStream.java—getString()andcreateAt(int)accessors for cross-packageindexOfTests
src/foam/lib/json/test/ParserOptimizationTest.js— 23-case consolidated regression guardsrc/foam/lib/json/test/tests.jrl— register the new testsrc/pom.js—CharLiteralandParserOptimizationTestentriesInstrumentation
src/foam/dao/F3FileJournal.js— per-phase nanos, op counters,commentsSkipped, fast comment check,logPhasePm_helper. Keeps the existingparseXcontext derivation andOP_CREATEfall-through intoOP_PUT.src/foam/dao/FileJournal.js— same instrumentation shape, single-threaded.Tests
ParserOptimizationTest(23 assertions): passes against the unchanged parsers and against every cherry-pick in sequence.run-testssuite: 1904 pass, 52 fail. All 52 failures areCSSAuditTesthits oncolor:/style:inHealthStatus.js,CapabilityJunctionStatus.js,Flow.js,Upload.js,DashboardSinks.js, etc. — pre-existing onupstream/development, none of which this branch touches.Commit sequence
Structured for bisectable review — each commit preserves green tests on the prior one:
test: add ParserOptimizationTest guarding parser primitives(baseline, passes on unchanged code)perf: optimize FOAM parser internals — 2x throughput improvement(DoubleParser + StringParser intern removal +CharLiteral+ inline SKIP)perf: add indexOf fast path to StringParser for no-escape stringsperf: inline property value parser and comma separator in ModelParserFactoryfeat: per-phase PM instrumentation for journal replay