{"id":51691435,"url":"https://github.com/ericsssan/ic-fuzzer","last_synced_at":"2026-07-16T02:34:08.869Z","repository":{"id":361758174,"uuid":"1255677254","full_name":"ericsssan/ic-fuzzer","owner":"ericsssan","description":"Diagnose \u0026 fix V8 megamorphic deoptimizations in Node/TypeScript: runnable example, benchmark, raw --trace-deopt/--log-ic tracing, strict TS, JIT-friendliness lint, and a CI deopt-gate. Companion to the article 'From Production Flamegraph to Fixed Megamorphic Call Site'.","archived":false,"fork":false,"pushed_at":"2026-06-14T14:37:45.000Z","size":147,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-07-16T02:33:59.764Z","etag":null,"topics":["deoptimization","jit","nodejs","performance","typescript","v8"],"latest_commit_sha":null,"homepage":null,"language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ericsssan.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-01T04:29:18.000Z","updated_at":"2026-06-14T14:37:49.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/ericsssan/ic-fuzzer","commit_stats":null,"previous_names":["ericsssan/ts-jit-deopt","ericsssan/ic-fuzzer"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/ericsssan/ic-fuzzer","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ericsssan%2Fic-fuzzer","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ericsssan%2Fic-fuzzer/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ericsssan%2Fic-fuzzer/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ericsssan%2Fic-fuzzer/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ericsssan","download_url":"https://codeload.github.com/ericsssan/ic-fuzzer/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ericsssan%2Fic-fuzzer/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35528482,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-16T02:00:06.687Z","response_time":83,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["deoptimization","jit","nodejs","performance","typescript","v8"],"created_at":"2026-07-16T02:34:07.970Z","updated_at":"2026-07-16T02:34:08.863Z","avatar_url":"https://github.com/ericsssan.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# ic-fuzzer\n\nRunnable companion to **[From Production Flamegraph to Fixed Megamorphic Call Site](./ARTICLE.md)** — a walkthrough of diagnosing and fixing V8 JIT deoptimization in Node.js.\n\n*Profilers tell you where time goes. They don't tell you why the JIT gave up.*\n\n---\n\nThere's a class of production performance problem that profilers can locate but not explain. The flamegraph is unambiguous — `handleEvent` is eating 40% of CPU. The function looks fine. Local benchmarks look fine. The problem isn't in the code; it's in what the runtime decided to do with it.\n\nV8 optimizes by speculating on object shapes. When too many distinct shapes flow through a property access, V8 gives up on the fast path — megamorphism — and falls back to a hashtable lookup on every access. This doesn't show up in source. It's a runtime fact, invisible to the reader and undetectable by profiler sampling.\n\nThis repo shows the full loop: find it with `--log-ic`, fix it at the shape boundary, enforce it in CI.\n\n```\nevents: 2,000,000, hot iterations: 20\n\nbroken   229.4ms   (megamorphic loads, un-inlinable call)\nfixed     54.5ms   (monomorphic loads, inlined)        ← ~4.2× faster\n```\n\nThe fix is one line at the ingestion boundary. `broken/handler.js` and `fixed/handler.js` are byte-for-byte identical.\n\n---\n\n## Running this repo\n\n```bash\nnpm install\n\nnpm run bench          # broken vs fixed timing (~4-5×)\nnpm run typecheck      # strict tsc on the TypeScript example\nnpm run lint           # static JIT-friendliness rules (--max-warnings=0)\nnpm run trace:broken   # raw --trace-deopt --log-ic for the broken hot path\nnpm run trace:fixed    # raw --trace-deopt --log-ic for the fixed hot path\nnpm run gate           # CI deopt-gate on the fixed path  -\u003e PASS\nnpm run gate:broken    # CI deopt-gate on the broken path -\u003e FAIL\nnpm run ts             # the TypeScript version\n```\n\n---\n\n## ic-fuzzer\n\nThis repo also ships `ic-fuzzer`, a tool that finds the minimal set of object shapes that push a function's inline caches to megamorphic. It uses IC severity — not crashes or coverage — as the feedback signal, which makes it novel: no existing fuzzer does this.\n\n### How V8 executes your code\n\nV8 runs JavaScript in three tiers:\n\n```\nIgnition (interpreter) → Maglev (mid-tier JIT) → Turbofan (optimizing compiler)\n```\n\nAt each property access (`.type`, `.id`, etc.), **Ignition** maintains an **inline cache (IC)** — a record of which object shapes (hidden classes) that site has seen. `monomorphic` = 1 shape, `polymorphic` = 2–4, `megamorphic` = 5+.\n\nWhen a function gets hot, **Turbofan** reads the IC feedback Ignition collected, then *replaces* the IC with specialized machine code. Monomorphic feedback → direct memory load (fast). Megamorphic feedback → generic hash-table lookup (slow, but still \"optimized\").\n\n**ic-fuzzer's purpose:** find input shapes that push a call site to megamorphic *during Ignition*, before Turbofan reads the accumulated feedback. The ICs disappear after Turbofan compiles — but their legacy is baked into the generated machine code. Megamorphic IC feedback causes Turbofan to emit permanently slower code.\n\n**What \"no IC data\" means:**\n\n| Situation | Cause | What to do |\n|---|---|---|\n| Function never called | Wrong export name, or seed doesn't trigger the call path | Fix the seed |\n| `X`/no_feedback state | Turbofan compiled before the probe ran; IC sites replaced by specialized code | Use `--no-turbofan` to observe Ignition-phase ICs |\n| No property accesses | Function doesn't read properties on its argument | ic-fuzzer doesn't apply |\n\n```bash\ncd ic-fuzzer\nnode bin/ic-fuzzer.js ../broken/handler.js handleEvent \\\n  --seed='{\"type\":\"click\",\"id\":1,\"value\":2}'\n```\n\n```\n[ic-fuzzer] minimal set: 5 shape(s)  (0 shrink step(s))\n\n   1. literal:type-id-value         { type, id, value }  literal\n   2. incr:type-value-id            e.type; e.value; e.id  incremental\n   3. incr:id-type-value            e.id; e.type; e.value  incremental\n   4. incr:value-type-id            e.value; e.type; e.id  incremental\n   5. incr:value-id-type            e.value; e.id; e.type  incremental\n\n[ic-fuzzer] IC sites at megamorphic:\n    [MEGAMORPHIC ]  .id     handleEvent  (broken/handler.js:10:16)\n    [MEGAMORPHIC ]  .type   handleEvent  (broken/handler.js:10:32)\n    [MEGAMORPHIC ]  .value  handleEvent  (broken/handler.js:10:52)\n```\n\nUse `--watch=\u003cfile\u003e` when your entry point is a thin wrapper around a library — it redirects IC collection to the file you actually care about:\n\n```bash\nnode bin/ic-fuzzer.js test/fixtures/picomatch-scan-entry.js scanDirect \\\n  --seed='{\"pattern\":\"**/*.{js,ts}\",\"parts\":false,...}' \\\n  --watch=../node_modules/picomatch/lib/scan.js\n```\n\nOther flags: `--target=polymorphic`, `--corpus=\u003cfile\u003e`, `--collector=\u003cfile\u003e`, `--dry-run`, `--rng=\u003cn\u003e` (reproduce), `--runs=\u003cn\u003e`, `--count=\u003cn\u003e`, `--iters=\u003cn\u003e`, `--function=\u003cname\u003e`, `--report-maps`, `--trace-reads`, `--trace-deopt`, `--no-turbofan`, `--max-maps=N`.\n\n### The full optimization workflow\n\nic-fuzzer covers the complete find → quantify → fix → verify loop:\n\n```bash\n# 1. Find it\nic-fuzzer ./handler.js handleEvent --seed='{\"type\":\"click\",\"id\":1,\"value\":2}'\n# → minimal set: 5 shapes, IC sites: .id .type .value\n\n# 2. Quantify — is this worth fixing? (--bench compares mono vs mixed-shape timing)\nic-fuzzer ./handler.js handleEvent --seed='...' --bench\n# → [ic-fuzzer] bench: mono=54ms  mixed=229ms  Δ=4.2×\n\n# 3. Fix it — write a boundary normalizer (e.g. toEvent()) that coerces inputs\n#    to a single canonical shape (class instance or literal) before the hot path\n\n# 4. Verify — CI gate that fails if any IC rises above monomorphic\nic-fuzzer ./handler.js handleEvent --seed='...' --assert-max=monomorphic\n# → [ic-fuzzer] PASS — handleEvent stays within monomorphic (exit 0)\n```\n\n**Why `fixed/handler.js` still reports megamorphic without a gate.**\nic-fuzzer feeds plain objects directly to the target function, bypassing any boundary normalizer. If your fix is a `toEvent()` that coerces raw inputs to a `new Event(...)`, running ic-fuzzer on `handleEvent` after the fix will still report megamorphic — because ic-fuzzer never calls `toEvent`. This is expected: the megamorphic reads were *moved* to the boundary, not eliminated.\n\nThe right targets after a boundary-normalization fix:\n- `handleEvent` with `--assert-max=monomorphic` — verifies the hot path is now clean\n- `toEvent` (without `--assert-max`) — verifies the boundary does what it should: finds megamorphism, confirming the coalescing reads are confined there\n\n### Programmatic API\n\n`ic-fuzzer` exposes its full internals as a Node module:\n\n```js\nconst { fuzz, probe, bench, derive, fromCorpus, SEV_LABEL } = require('ic-fuzzer');\n```\n\n**`probe(fnFile, fnName, strategies, watchFile?, count?, iters?)` → `{ severity, ics, hasICs, crashed, error }`**\n\nRun one shaped probe and get back raw IC severity. Use this for a CI assertion that a hot path stays monomorphic:\n\n```js\n// ci/monomorphic-gate.js\nconst path = require('path');\nconst { probe, derive } = require('ic-fuzzer');\n\nconst file = path.resolve('src/handler.js');\nconst seed = { type: 'click', id: 1, value: 2 };\n\n(async () =\u003e {\n  // Five distinct shapes — enough to trigger megamorphism if the IC is polymorphic\n  const five = derive(seed).slice(0, 5);\n  const { severity } = await probe(file, 'handleEvent', five);\n  if (severity \u003e= 3) {\n    console.error(`handleEvent is megamorphic (severity ${severity}) — normalize at the boundary`);\n    process.exit(1);\n  }\n  console.log('handleEvent is JIT-friendly');\n})();\n```\n\n**`fuzz(opts)` → `{ found, minimalStrategies, ics, numShrinks, rngSeed, anyICs, anyMonomorphic, numCrashes }`**\n\nRun the full fast-check search + shrink loop. Useful for fuzzing internal closures or class methods that aren't exported directly — wrap them in a thin harness file:\n\n```js\n// test/fuzz-internal.js\nconst path   = require('path');\nconst { fuzz, derive } = require('ic-fuzzer');\n\n// harness.js exports a wrapper around the private function under test\nconst file = path.resolve('test/fuzz-harness.js');\n\n(async () =\u003e {\n  const result = await fuzz({\n    fnFile:     file,\n    fnName:     'processNode',\n    strategies: derive({ type: 'Identifier', name: 'x', start: 0, end: 1 }),\n    targetSev:  3,   // 2 = polymorphic, 3 = megamorphic\n    numRuns:    200,\n    onRun({ subset, severity, phase }) {\n      if (severity \u003e= 3) console.log('  megamorphic:', subset.join(', '));\n    },\n  });\n\n  if (result.found) {\n    console.error(`megamorphic set: ${result.minimalStrategies.map(s =\u003e s.name).join(', ')}`);\n    process.exit(1);\n  }\n  console.log('no megamorphism found');\n})();\n```\n\n---\n\n## Layout\n\n```\nts-jit-deopt/\n  ARTICLE.md         ← the full article\n  event-source.js    ← heterogeneous event source (pure shape variation)\n  bench.js           ← broken vs fixed timing harness\n  broken/\n    handler.js       ← the hot path (receives many shapes → megamorphic)\n    drive.js         ← driver\n  fixed/\n    event.js         ← canonical shape + boundary normalizer\n    handler.js       ← identical hot path (receives one shape → monomorphic)\n    drive.js         ← driver (normalized at the boundary)\n  ci/\n    deopt-gate.js    ← fail CI on mono→megamorphic regression\n    gate-driver.js   ← replays the corpus through the chosen handler\n  ic-fuzzer/\n    bin/ic-fuzzer.js ← CLI: ic-fuzzer \u003cfile\u003e \u003cexport\u003e --seed=\u003cjson\u003e [--watch=\u003cfile\u003e]\n    src/mutate.js    ← derives shape strategies from a seed object\n    src/probe.js     ← subprocess driver: runs under --log-ic, parses IC severity\n    src/fuzzer.js    ← fast-check loop: search + shrink to minimal shape set\n    src/reporter.js  ← progress, result formatting, corpus.json output\n  fuzzer/            ← legacy shape fuzzer (predates ic-fuzzer)\n  ts/\n    example.ts       ← the TypeScript version (boundary encoded in types)\n  .github/workflows/\n    deopt-gate.yml   ← drop-in GitHub Actions job\n```\n\n## License\n\nMIT © 2026 Eric San. See [LICENSE](./LICENSE).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fericsssan%2Fic-fuzzer","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fericsssan%2Fic-fuzzer","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fericsssan%2Fic-fuzzer/lists"}