Skip to content

perf: 2x journal replay throughput via parser optimizations + per-phase PM#4980

Open
abdelaziz-mahdy wants to merge 5 commits intodevelopmentfrom
journal-perf-parser-only
Open

perf: 2x journal replay throughput via parser optimizations + per-phase PM#4980
abdelaziz-mahdy wants to merge 5 commits intodevelopmentfrom
journal-perf-parser-only

Conversation

@abdelaziz-mahdy
Copy link
Copy Markdown
Collaborator

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 a StringBuilder, several intermediate combinator objects, and a String.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.json and foam.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 — replace StringBuilder + Double.valueOf with direct long arithmetic. Zero heap allocation per number; preserves IEEE-754 negative-zero sign bit; stays within one ULP of Double.valueOf on irrational decimals.
  • StringParser — drop String.intern() (every parsed string hit the JVM's global intern table under C++ synchronization) and add an indexOf(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 generic AbstractLiteral.parse() loop and its String.charAt. AbstractLiteral.create("c") now returns a CharLiteral for any one-char input.

Regression tests (ParserOptimizationTest): 23 cases locking in pre-optimization behavior — scientific notation, leading zeros, negative-zero sign bit, Double.MAX_VALUE boundary, irrational-decimal ULP bound, escape sequences, unicode escapes, the three string delimiter styles, and JSON roundtrip through FObjectParser+ModelParserFactory covering 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: F3FileJournal and FileJournal now accumulate four timing buckets (getEntry / parse / findMerge / daoWrite) and emit one PM row per phase at replay completion. The replay-complete log line adds opCreate / opPut / opPutMerged / opRemove / commentsSkipped counters. Comment check switches from a Pattern.matcher(entry).matches() regex call to entry.charAt(0) == '/' — superset-safe for the existing comment regex since every comment starts with / and data entries start with c / p / r / v.

Changes

Parser

  • src/foam/lib/json/DoubleParser.java — arithmetic accumulation, no StringBuilder, no Double.valueOf
  • src/foam/lib/json/StringParser.java — drop intern(), add indexOf fast path
  • src/foam/lib/json/ModelParserFactory.java — inline SKIP, inline property+comma, inline value parser
  • src/foam/lib/parse/AbstractLiteral.java — factory returns CharLiteral for single-char inputs
  • src/foam/lib/parse/CharLiteral.java — new, specialized single-char literal parser
  • src/foam/lib/parse/StringPStream.javagetString() and createAt(int) accessors for cross-package indexOf

Tests

  • src/foam/lib/json/test/ParserOptimizationTest.js — 23-case consolidated regression guard
  • src/foam/lib/json/test/tests.jrl — register the new test
  • src/pom.jsCharLiteral and ParserOptimizationTest entries

Instrumentation

  • src/foam/dao/F3FileJournal.js — per-phase nanos, op counters, commentsSkipped, fast comment check, logPhasePm_ helper. Keeps the existing parseX context derivation and OP_CREATE fall-through into OP_PUT.
  • src/foam/dao/FileJournal.js — same instrumentation shape, single-threaded.

Tests

  • Targeted parser + JSON server suite (11 tests, 183 assertions): all pass.
  • ParserOptimizationTest (23 assertions): passes against the unchanged parsers and against every cherry-pick in sequence.
  • Full run-tests suite: 1904 pass, 52 fail. All 52 failures are CSSAuditTest hits on color: / style: in HealthStatus.js, CapabilityJunctionStatus.js, Flow.js, Upload.js, DashboardSinks.js, etc. — pre-existing on upstream/development, none of which this branch touches.

Commit sequence

Structured for bisectable review — each commit preserves green tests on the prior one:

  1. test: add ParserOptimizationTest guarding parser primitives (baseline, passes on unchanged code)
  2. perf: optimize FOAM parser internals — 2x throughput improvement (DoubleParser + StringParser intern removal + CharLiteral + inline SKIP)
  3. perf: add indexOf fast path to StringParser for no-escape strings
  4. perf: inline property value parser and comma separator in ModelParserFactory
  5. feat: per-phase PM instrumentation for journal replay

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.
// 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; }
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this still work with multi-line comments?

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure the old one supports multi-line comments, despite the REGEX supporting them.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants