Skip to content
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

Dead code elimination idea: unused private methods in classes #771

Closed
Jarred-Sumner opened this issue Feb 8, 2021 · 5 comments
Closed

Comments

@Jarred-Sumner
Copy link
Contributor

Jarred-Sumner commented Feb 8, 2021

Visual Studio Code indicates when private functions in TypeScript files are unused:
image

Currently, unused private methods are included in output bundles (with minifySyntax enabled):
image

Would it be possible to safely remove unused private methods from classes?

I suppose it's possible that you could use string syntax to call the function this["checkForObstructingMeshes"]() or even this["checkForObstructing" + "Meshes"] . Does that currently work with exported functions to prevent elimination?

@evanw
Copy link
Owner

evanw commented Feb 9, 2021

The TypeScript type system is unsound (by design) so type annotations cannot be relied on for things like this. Here is an example for which the TypeScript compiler gives no type errors but incorrectly marks the used method as unused:

class Foo {
  // TypeScript says: 'foo' is declared but its value is never read
  private foo() {}
}

class Bar {
  public foo() {}
}

function messUpTheArray(arr: Array<Foo | Bar>): void {
  arr.push(new Foo);
}

const bars: Array<Bar> = [new Bar];
messUpTheArray(bars);

for (let bar of bars) {
  bar.foo()
}

So this approach doesn't work unfortunately. I'm going to close this issue both for this reason and because giving esbuild a type system is out of scope.

A tangent about optimizing JavaScript

I like building optimizing compilers and I have thought a lot about this already. The way to do something like this safely would be to throw out all of the TypeScript annotations and start with the JavaScript, then run a complicated whole-program analysis with your own type system to come up with your own actually accurate types. This is sort of what happens when you run Google Closure Compiler in advanced mode, although it also has its own JavaScript language subset and your code can easily break if you do anything outside of their special subset language.

One could imagine something that does this complicated analysis of JavaScript code in a fully safe and automatic manner without also including something like Google Closure Compiler's language subset. I have thought about building something like that before. But it would be a big undertaking and would be a very different tool than esbuild.

And to be fully safe while also being fully automatic, it would have to also be pretty pessimistic. For example, it's not necessarily clear that passing a CameraInputs instance to console.log means that checkForObstructingMeshes is still unused because many libraries monkey-patch console.log with their own code and that code may iterate over all methods and end up accessing checkForObstructingMeshes in some important way. That is one reason why Google Closure Compiler requires you to have "extern" files that declare the properties and type annotations of the external code you use.

But at that point you are basically creating another language on top of JavaScript. So the conclusion I have reached after going down this train of thought is that the most ergonomic way to do this (write highly-optimizable code that compiles to JavaScript) is to make another language with a more strict type system and compile that down to JavaScript instead. Then you can make certain guarantees such as "the compiler can see all uses of every method" and perform advanced optimizations such as method inlining and devirtualization. Devirtualization means turning dynamically-bound methods into statically-bound functions, which can then be inlined and/or eliminated as dead code, and is a very powerful optimization.

Google Closure Compiler is one example of an approach like this, and does do these optimizations. Another example is a compile-to-JavaScript language I have built that does this: Skew. We use it at Figma (where I work) for the mobile client, where download size, initialization time, and run-time performance is a big deal. WebAssembly unfortunately has a huge download size and initialization time cost. Skew allowed us to write code that is smaller with a lower initialization time than either WebAssembly or JavaScript. I have tried switching us away from Skew to TypeScript in the past since the TypeScript ecosystem obviously has more tooling, but we didn't end up switching because removing Skew's optimizing compiler was a 1.5-2x performance hit (if I recall correctly).

If you're looking for this level of optimization, it is possible and you can get big wins but you potentially need to use very different tools. I'm not saying you should use Skew but I would consider checking out tools outside of the TypeScript ecosystem such as Google Closure Compiler. It may also be possible to get this level of optimization by compiling other languages such as Java or ReasonML to JavaScript (I have never tried myself), although you have to be careful about the additional overhead of the language runtime.

@evanw evanw closed this as completed Feb 9, 2021
@Jarred-Sumner
Copy link
Contributor Author

The TypeScript type system is unsound (by design) so type annotations cannot be relied on for things like this.

I confused TypeScript's private modifier with Private class fields. But it makes since why that detail wouldn't really change much

If you're looking for this level of optimization, it is possible and you can get big wins but you potentially need to use very different tools

I've thought a lot about using a different language like V or Nim. V has a very energetic community and Nim seems to have Rust-like performance with Python-like syntax.

But the memory model with WASM kind of sucks. If you're generating meshes from voxels in WASM in multiple workers, you have to copy voxel data to WASM multiple times (potentially megabytes of data), and then copy the geometry data out of WASM when meshing is complete. And if the browser doesn't support SharedArrayBuffer (Safari doesn't and Firefox has a 1,000 SAB object limit independent of size), the situation is worse since you copy it again when going from main -> worker.

@rkirov
Copy link

rkirov commented Feb 20, 2021

We reached a similar conclusion in our exploration of using TS types for optimization. Sadly, our conclusions are burried in the mega-thread microsoft/TypeScript#8 (comment). Look for comments from - evmar, mprobst and me.

@evanw
Copy link
Owner

evanw commented Feb 20, 2021

Interesting! Thanks for the link.

@concavelenz
Copy link

The type based optimizations are secondary for Closure Compiler's advanced mode (and is a separate option). The key assumption that it makes it can see all the references to class properties (it is less aggressive with regard to other property definitions). This bans a bunch of reflective patterns and encourages larger units of compilation to minimize the surface exposed.

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

No branches or pull requests

4 participants