Skip to content

perf(typescript-language): stop tsserver formatting discarded log messages - #3256

Draft
JoshuaKGoldberg wants to merge 1 commit into
mainfrom
perf/tsserver-discarded-logging
Draft

perf(typescript-language): stop tsserver formatting discarded log messages#3256
JoshuaKGoldberg wants to merge 1 commit into
mainfrom
perf/tsserver-discarded-logging

Conversation

@JoshuaKGoldberg

Copy link
Copy Markdown
Collaborator

SASSAFRAS

PR Checklist

Overview

@typescript-eslint/project-service builds its tsserver logger with hasLevel: () => true while loggingEnabled() correctly reports whether any debug namespace is on. tsserver uses hasLevel() as the guard before formatting a message, so with no DEBUG namespaces enabled it still builds every internal log string and hands it to a disabled debug namespace.

There is no supported seam for this: createProjectService's settings are { host, jsDocParsingMode, options, tsconfigRootDir }, ProjectServiceOptions has no logger field, and the host we pass becomes tsserver's ServerHost, not its logger. ts.server.ProjectService#logger and ts.server.Logger#hasLevel are both public TypeScript API, though, so the returned service's hasLevel can be pointed at the loggingEnabled() it should have agreed with in the first place:

service.logger.hasLevel = () => service.logger.loggingEnabled();

With any of the typescript-eslint:project-service:tsserver:* namespaces enabled this is a no-op, so debugging tsserver still works exactly as before. With them disabled, tsserver skips the guarded logging entirely.

The win is larger than the issue estimated, because the issue counted only msg and debug self time. The dominant cost is ProjectService#printProjects, which is gated on hasLevel(LogLevel.normal) and is called after every openClientFile/closeClientFile — so on a 1024-file run it walks every open file and every project ~2000 times, building strings nobody reads.

CPU profile of files-1024-rules-many, inclusive/self time for the affected frames:

frame main this branch
printProjects (typescript.js) 193.0 ms incl / 27.0 ms self not sampled
msg (createProjectService.js) 51.3 ms incl / 23.8 ms self 2.4 ms incl / 1.2 ms self
info (createProjectService.js) 69.9 ms incl / 18.7 ms self not sampled
debug (debug/common.js) 24.9 ms incl / 19.0 ms self 1.3 ms incl / 1.3 ms self

Measurements

hyperfine -N --warmup 2 -r 8, back-to-back on the same machine, using the #3024 fixture cases.

files rules main this branch delta
256 many 1.358 s ± 35 ms 1.289 s ± 42 ms −69 ms (−5.1%)
1024 many 3.077 s ± 41 ms 2.817 s ± 62 ms −260 ms (−8.4%)

Warm cache, same runs:

files rules main this branch delta
256 many 1.350 s ± 50 ms 1.306 s ± 26 ms −44 ms (−3.3%)
1024 many 3.108 s ± 56 ms 2.874 s ± 109 ms −234 ms (−7.5%)

Findings are byte-identical to main on files-256-rules-many and files-1024-rules-many (--cache-ignore --skip-formatting --skip-language-reports).

Not covered

  • This is a workaround in Flint, not the real fix. The real fix is upstream in @typescript-eslint/project-service: in src/createProjectService.ts, the logger's hasLevel: () => true should defer to the same namespace check loggingEnabled already does (logTsserverInfo.enabled || logTsserverErr.enabled || logTsserverPerf.enabled). The existing comment there explains why it was left as truedebug has no levels — but "no levels" only justifies not distinguishing levels, not claiming a level is enabled when no namespace is. Every hasLevel call site in tsserver guards logging only, so agreeing with loggingEnabled costs nothing but the same all-or-nothing granularity the writer already has. Once that ships, this line should be deleted.
  • Alternatively Flint could construct ts.server.ProjectService itself and own the logger outright, which would also let it drop the dependency. That means taking on the stubbed system, plugin blocking, setHostConfiguration, and the defaultProjectsetCompilerOptionsForInferredProjects path (which needs @typescript-eslint/tsconfig-utils' getParsedConfigFile reimplemented). That felt like a lot of surface to own for a logging flag, so I did not do it.
  • No unit test: createFileFactory has no test file today and the behavior is "tsserver does less work", which is only observable in a profile.

…sages

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@netlify

netlify Bot commented Aug 19, 2026

Copy link
Copy Markdown

Deploy Preview for flint-fyi ready!

Name Link
🔨 Latest commit 2ec6c82
🔍 Latest deploy log https://app.netlify.com/projects/flint-fyi/deploys/6a85ee7575f02b00080cc864
😎 Deploy Preview https://deploy-preview-3256--flint-fyi.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@changeset-bot

changeset-bot Bot commented Aug 19, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 2ec6c82

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@codecov

codecov Bot commented Aug 19, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

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.

🐛 Bug: tsserver builds log messages that are always discarded

1 participant