From 641c0347fe6af872f69a7b308b6174a36e35a65a Mon Sep 17 00:00:00 2001 From: no-yan <63000297+no-yan@users.noreply.github.com> Date: Thu, 20 Aug 2026 15:56:16 +0900 Subject: [PATCH] Cache repeated computations in program construction hot paths - fileLoader.toPath: called once per import edge with heavily repeated file names; cache the canonical path per loader. - Resolver conditions: newResolutionState rebuilt the same conditions slice for every resolution; precompute the import/require variants per resolver (project reference redirects fall back to GetConditions). The import/require classification is factored into conditionsUseImport, shared with GetConditions, and a test asserts the cached slices match GetConditions for every resolution mode. Co-Authored-By: Claude Fable 5 --- tsc/internal/compiler/fileloader.go | 11 +++- tsc/internal/module/resolver.go | 50 +++++++++++++++---- .../module/resolver_conditions_test.go | 35 +++++++++++++ 3 files changed, 85 insertions(+), 11 deletions(-) create mode 100644 tsc/internal/module/resolver_conditions_test.go diff --git a/tsc/internal/compiler/fileloader.go b/tsc/internal/compiler/fileloader.go index c89d06051692c..09b3103edebd7 100644 --- a/tsc/internal/compiler/fileloader.go +++ b/tsc/internal/compiler/fileloader.go @@ -68,6 +68,10 @@ type fileLoader struct { pathForLibFileCache collections.SyncMap[string, *LibFile] pathForLibFileResolutions collections.SyncMap[tspath.Path, *libResolution] + // toPath is called repeatedly for the same file names (once per import edge); + // caching avoids re-normalizing and re-lowercasing the same path. + toPathCache collections.SyncMap[string, tspath.Path] + // contentMapperMu guards the content-mapper bookkeeping below, which is written concurrently as // content-mapped files are parsed across worker goroutines. contentMapperMu sync.Mutex @@ -213,7 +217,12 @@ func processAllProgramFiles( } func (p *fileLoader) toPath(file string) tspath.Path { - return tspath.ToPath(file, p.opts.Host.GetCurrentDirectory(), p.opts.Host.FS().UseCaseSensitiveFileNames()) + if path, ok := p.toPathCache.Load(file); ok { + return path + } + path := tspath.ToPath(file, p.opts.Host.GetCurrentDirectory(), p.opts.Host.FS().UseCaseSensitiveFileNames()) + p.toPathCache.Store(file, path) + return path } func (p *fileLoader) addRootTask(fileName string, libFile *LibFile, includeReason *FileIncludeReason) { diff --git a/tsc/internal/module/resolver.go b/tsc/internal/module/resolver.go index b408b7d4c027a..74f3c2990eb27 100644 --- a/tsc/internal/module/resolver.go +++ b/tsc/internal/module/resolver.go @@ -131,14 +131,14 @@ func newResolutionState( case core.ModuleResolutionKindNode16: state.features = NodeResolutionFeaturesNode16Default state.esmMode = resolutionMode == core.ModuleKindESNext - state.conditions = GetConditions(compilerOptions, resolutionMode) + state.conditions = resolver.getConditions(compilerOptions, resolutionMode) case core.ModuleResolutionKindNodeNext: state.features = NodeResolutionFeaturesNodeNextDefault state.esmMode = resolutionMode == core.ModuleKindESNext - state.conditions = GetConditions(compilerOptions, resolutionMode) + state.conditions = resolver.getConditions(compilerOptions, resolutionMode) case core.ModuleResolutionKindBundler: state.features = getNodeResolutionFeatures(compilerOptions) - state.conditions = GetConditions(compilerOptions, resolutionMode) + state.conditions = resolver.getConditions(compilerOptions, resolutionMode) } return state } @@ -161,6 +161,29 @@ type Resolver struct { projectName string extraExtensions []string // reportDiagnostic: DiagnosticReporter + + // Conditions depend only on (options, import/require), so they are computed + // once for the resolver's own options; project reference redirects (rare) + // fall back to GetConditions. These slices are shared across all resolutions + // of this resolver and must be treated as read-only. + esmConditions []string + cjsConditions []string +} + +func (r *Resolver) initConditionCaches() { + // Clip so that an accidental append by a consumer cannot overwrite the shared backing array. + r.esmConditions = slices.Clip(GetConditions(r.compilerOptions, core.ModuleKindESNext)) + r.cjsConditions = slices.Clip(GetConditions(r.compilerOptions, core.ModuleKindCommonJS)) +} + +func (r *Resolver) getConditions(options *core.CompilerOptions, resolutionMode core.ResolutionMode) []string { + if options != r.compilerOptions { + return GetConditions(options, resolutionMode) + } + if conditionsUseImport(options, resolutionMode) { + return r.esmConditions + } + return r.cjsConditions } type ResolverOptions struct { @@ -174,7 +197,7 @@ func NewResolver( projectName string, extraExtensions []string, ) *Resolver { - return &Resolver{ + r := &Resolver{ host: host, caches: newCaches(host.GetCurrentDirectory(), host.FS().UseCaseSensitiveFileNames(), options), compilerOptions: options, @@ -182,6 +205,8 @@ func NewResolver( projectName: projectName, extraExtensions: extraExtensions, } + r.initConditionCaches() + return r } func NewResolverWithOptions( @@ -197,6 +222,7 @@ func NewResolverWithOptions( typingsLocation: typingsLocation, projectName: projectName, } + r.initConditionCaches() if opts.PackageJsonCache != nil { r.packageJsonInfoCache = opts.PackageJsonCache } else { @@ -1935,13 +1961,17 @@ func (r *resolutionState) getTraceFunc() func(m *diagnostics.Message, args ...an return nil } +// conditionsUseImport reports whether GetConditions yields the "import" condition +// (as opposed to "require") for this options/mode pair. Single source of truth for +// the mode classification, shared with Resolver.getConditions. +func conditionsUseImport(options *core.CompilerOptions, resolutionMode core.ResolutionMode) bool { + return resolutionMode == core.ModuleKindESNext || + (resolutionMode == core.ModuleKindNone && options.GetModuleResolutionKind() == core.ModuleResolutionKindBundler) +} + func GetConditions(options *core.CompilerOptions, resolutionMode core.ResolutionMode) []string { - moduleResolution := options.GetModuleResolutionKind() - if resolutionMode == core.ModuleKindNone && moduleResolution == core.ModuleResolutionKindBundler { - resolutionMode = core.ModuleKindESNext - } conditions := make([]string, 0, 3+len(options.CustomConditions)) - if resolutionMode == core.ModuleKindESNext { + if conditionsUseImport(options, resolutionMode) { conditions = append(conditions, "import") } else { conditions = append(conditions, "require") @@ -1950,7 +1980,7 @@ func GetConditions(options *core.CompilerOptions, resolutionMode core.Resolution if options.NoDtsResolution != core.TSTrue { conditions = append(conditions, "types") } - if moduleResolution != core.ModuleResolutionKindBundler { + if options.GetModuleResolutionKind() != core.ModuleResolutionKindBundler { conditions = append(conditions, "node") } conditions = core.Concatenate(conditions, options.CustomConditions) diff --git a/tsc/internal/module/resolver_conditions_test.go b/tsc/internal/module/resolver_conditions_test.go new file mode 100644 index 0000000000000..40188e2b5c6ee --- /dev/null +++ b/tsc/internal/module/resolver_conditions_test.go @@ -0,0 +1,35 @@ +package module + +import ( + "slices" + "testing" + + "github.com/microsoft/TypeScript/tsc/internal/core" +) + +// The cached esm/cjs conditions must stay equivalent to GetConditions for every +// resolution mode, since getConditions re-derives the import/require selection. +func TestGetConditionsCacheMatchesGetConditions(t *testing.T) { + t.Parallel() + + optionsList := []*core.CompilerOptions{ + {ModuleResolution: core.ModuleResolutionKindNode16}, + {ModuleResolution: core.ModuleResolutionKindNodeNext}, + {ModuleResolution: core.ModuleResolutionKindBundler}, + {ModuleResolution: core.ModuleResolutionKindBundler, NoDtsResolution: core.TSTrue}, + {ModuleResolution: core.ModuleResolutionKindNode16, CustomConditions: []string{"custom1", "custom2"}}, + } + modes := []core.ResolutionMode{core.ModuleKindNone, core.ModuleKindCommonJS, core.ModuleKindESNext} + + for _, options := range optionsList { + r := &Resolver{compilerOptions: options} + r.initConditionCaches() + for _, mode := range modes { + got := r.getConditions(options, mode) + want := GetConditions(options, mode) + if !slices.Equal(got, want) { + t.Errorf("options=%+v mode=%v: getConditions=%v, GetConditions=%v", options, mode, got, want) + } + } + } +}