Skip to content

Improve performance of Debug.format functions #49487

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jun 10, 2022

Conversation

jakebailey
Copy link
Member

@jakebailey jakebailey commented Jun 10, 2022

These functions are not normally performance critical, however, the cleanup in #49485 removes the test harness' duplicate SyntaxKind formatter in favor of Debug's (which is more accurate, but slower), and this recovers the perf loss this causes.

This technically speeds up tests by 25%, but that is only there because #49485 slows down the tests by 25%. Sigh.

@jakebailey
Copy link
Member Author

I guess the saving grace is that while debugging, these are called a lot if you have the nice debugger helpers enabled, so that should get faster.

@jakebailey jakebailey merged commit 678afe8 into microsoft:main Jun 10, 2022
@jakebailey jakebailey deleted the ts-debug-format-caching branch June 10, 2022 23:15
@Andarist
Copy link
Contributor

@jakebailey out of curiosity - are these called by methods like __debugTypeToString etc?

@jakebailey
Copy link
Member Author

Some of them, yes, but other helpers like that are written without using the format functions. I haven't looked into the performance of everything in Debug yet, this PR was mainly to enable removal of duplicate SyntaxKind formatting code without a performance hit.

@@ -301,19 +301,19 @@ namespace ts {
return members.length > 0 && members[0][0] === 0 ? members[0][1] : "0";
}
if (isFlags) {
let result = "";
const result: string[] = [];
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not sure if it's still the case, but in earlier v8 versions if you avoid allocating the array until you have the first element the performance is improved because it doesn't have to re-type the array.

const ar1 = []; // typed internally as a JS_ARRAY_TYPE of PACKED_SMI_ELEMENTS
ar1.push("foo"); // deoptimizes, re-types, now it's internally typed as an array of strings

// vs

const ar2 = ["foo"]; // typed internally as an array of strings

As a result, we have a lot of code that does either of the following:

let array: Foo[] | undefined;
...
if (array) {
  array.push(foo);
}
else {
  array = [foo];
}

// or
...
array = append(array, foo); // same as above, but in an inlinable function

That said, since this code isn't performance critical, it probably wouldn't help much.

Copy link
Member Author

Choose a reason for hiding this comment

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

That's an interesting idea; I know that this string addition itself was definitely bad, but I had never (when fixing this kind of code after pprof showing huge allocations) thought about creating a single array with one element to get v8 to do the right thing; the performance boost from just doing join was the main significant item.

Copy link
Contributor

Choose a reason for hiding this comment

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

IIRC, this was a performance optimization employed by @vladima back in the day. It still seems to be effective from what I can tell.

// Assuming enum objects do not change at runtime, we can cache the enum members list
// to reuse later. This saves us from reconstructing this each and every time we call
// a formatting function (which can be expensive for large enums like SyntaxKind).
const existing = enumMemberCache.get(enumObject);
Copy link
Contributor

Choose a reason for hiding this comment

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

As you mentioned, this is one of those things that only really matters for tests. In tsc there will never be a cached value since these functions should only ever be called right before a crash due to a debug assertion. It can possibly benefit the language service, but I'm not sure how often that would happen in practice.

Copy link
Member Author

Choose a reason for hiding this comment

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

The main benefit at runtime is the debug helpers that we define to use in VS Code's debugger in settings.json, where these helpers are called over and over to show the node types. Some things can be improved later, though.

Copy link
Contributor

Choose a reason for hiding this comment

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

is there any document describing how you debug TypeScript etc? I have my own ways (mostly using __debugTypeToString and __debugGetText in the debugger) - but I wonder if I'm missing some better techniques.

Copy link
Member Author

Choose a reason for hiding this comment

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

Making sure that customDescriptionGenerator is enabled in your launch.json is one thing related to this PR, but past that, I don't have any cool tricks besides a collection of helpers I used in the watch window (https://github.com/jakebailey/ts-debug-helpers), which is mainly about printing out nodes/the parser's scanner to more easily see what code is being operated on. Not type related at all.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Author: Team For Uncommitted Bug PR for untriaged, rejected, closed or missing bug
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants