Skip to content

POC: cache file entries to reduce heap allocation#548

Open
no-yan wants to merge 2 commits intooxc-project:mainfrom
no-yan:perf/cache-dir-entry
Open

POC: cache file entries to reduce heap allocation#548
no-yan wants to merge 2 commits intooxc-project:mainfrom
no-yan:perf/cache-dir-entry

Conversation

@no-yan
Copy link
Collaborator

@no-yan no-yan commented Dec 31, 2025

PoC for #295

This change reduces GC time by ~30%, by eliminating string concatenation in missing files check.

In the large codebase(kibana), program execution is 10% faster than before.
I'm not considering this should be merged, but I believe it could serve as a starting point for proposing a new approach to the tsgo team.

Note

Disclosure: I used some AI tools: ChatGPT and Gemini to help creating hypotheses and improve my English, and Claude to review my code.

Object allocation

Some execution paths still fall back to the original implementation, but overall allocations are significantly reduced.

Metric Before After
tryExtension / tryExtensionBits (flat%) 20.35% 0.46%
tryExtension / tryExtensionBits (cum%) 21.77% 0.48%

GC time

Metric Before After
runtime.gcBgMarkWorker.func2 (cum%) 24.71% 18.41%
Details

before:

(pprof) top 10 -cum
Showing nodes accounting for 8.06s, 19.79% of 40.72s total
Dropped 1064 nodes (cum <= 0.20s)
Showing top 10 nodes out of 341
      flat  flat%   sum%        cum   cum%
         0     0%     0%     23.60s 57.96%  runtime.systemstack
         0     0%     0%     10.06s 24.71%  runtime.gcBgMarkWorker.func2
     0.03s 0.074% 0.074%     10.06s 24.71%  runtime.gcDrain
         0     0% 0.074%      9.63s 23.65%  runtime.gcBgMarkWorker
         0     0% 0.074%      8.04s 19.74%  runtime.semawakeup
     8.03s 19.72% 19.79%      8.03s 19.72%  runtime.pthread_cond_signal
         0     0% 19.79%      7.99s 19.62%  runtime.notewakeup
         0     0% 19.79%      7.99s 19.62%  runtime.startm
         0     0% 19.79%      7.99s 19.62%  runtime.wakep
         0     0% 19.79%      7.35s 18.05%  runtime.ready

after:

(pprof) top 10 -cum
Showing nodes accounting for 7.31s, 20.70% of 35.31s total
Dropped 990 nodes (cum <= 0.18s)
Showing top 10 nodes out of 307
      flat  flat%   sum%        cum   cum%
         0     0%     0%     18.80s 53.24%  runtime.systemstack
         0     0%     0%      7.29s 20.65%  runtime.semawakeup
     7.28s 20.62% 20.62%      7.28s 20.62%  runtime.pthread_cond_signal
         0     0% 20.62%      7.20s 20.39%  runtime.wakep
         0     0% 20.62%      7.19s 20.36%  runtime.notewakeup
         0     0% 20.62%      7.19s 20.36%  runtime.startm
         0     0% 20.62%      6.59s 18.66%  runtime.ready
         0     0% 20.62%      6.50s 18.41%  runtime.gcBgMarkWorker.func2
     0.03s 0.085% 20.70%      6.50s 18.41%  runtime.gcDrain
         0     0% 20.70%      6.03s 17.08%  runtime.readyWithTime.goready.func1

Comment on lines +242 to +259
+// tryExtensionBits checks whether `extensionless + <extension>` exists, using pre-scanned cache.
+// If the file exists, it constructs and returns `resolved`; otherwise it returns continueSearching().
+//
+// Why this exists:
+// Module resolution is a hot path. A straightforward implementation tries multiple extensions by
+// concatenating strings (`extensionless + ext`) and then hitting the filesystem.
+// In Go, each concatenation allocates a new string, so doing this for *missing* candidates increases GC pressure.
+//
+// Optimization strategy:
+// - During a directory scan, we record "which extensions exist" for each extensionless path:
+// pathCache[extensionless] = bitset of possible extensions
+// - At lookup time, we first test the bitset; we only build the final path string when the extension is possible.
+//
+// Result:
+// Avoids allocations for non-existing extension candidates; we allocate only for candidates that may exist.
+//
+// See: https://www.typescriptlang.org/docs/handbook/modules/reference.html#the-moduleresolution-compiler-option
+func (r *resolutionState) tryExtensionBits(extension extensionBits, extensionless string, resolvedUsingTsExtension bool, _onlyrecordFailure bool) *resolved {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is the main logic.

@no-yan no-yan marked this pull request as ready for review December 31, 2025 08:33
@camc314 camc314 self-assigned this Dec 31, 2025
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