Differences between Dart compilers

The various Dart compilers and compilation modes have different optimizations. This can lead to differences in the recorded API usage between compilers.

Compiler Optimizations

The following compiler optimizations can influence the recorded API usage:

  • Tree-shaking / Dead code elimination: Code that is not used is removed from the compiled output. If an API is only used in code that is tree-shaken, the API usage will not be recorded. The compilers have different levels of sophistication in their tree-shaking algorithms. For example, dart compile exe might tree-shake a call, while dart2js might not.
  • Constant propagation and inlining: Constant propagation and inlining of other code might lead to record-use annotated API calls receiving more constant arguments or being promoted to static calls. These optimizations might also lead to tearoffs being promoted to static calls or const values showing up for call sites that were propagated there by the compiler. (The record-use annotated API itself should not be optimized away by inlining or constant propagation. The compilers preserve such APIs.)
  • Other optimizations: Other optimizations that can affect the recorded API usage include type inference and propagation, loop optimizations, and late variable initialization.

Loading Units and Code Partitioning

When deferred loading is used, compilers partition the application into separate loading units (e.g., separate JavaScript files or AOT loading units) that can be loaded dynamically.

How different compilers partition code and name these loading units can vary significantly:

  • Partitioning strategies: The algorithms and heuristics for grouping code into deferred loading units differ between compilers (such as VM AOT, dart2js, and dart2wasm). Consequently, a particular API usage might end up associated with different loading units depending on the compiler.
  • Loading unit names: Compilers use different naming schemes or identifiers for loading units (e.g., integers like 1 or 2, or file names like out.js_1.part.js).

Recommendations for Accurate Recording with package:record_use

The package:record_use library records API usage by analyzing the compiled output of a program. This means that the recorded API usage can vary depending on the compiler and compilation mode that is used. To ensure more accurate and consistent recording, consider the following recommendations:

  • @mustBeConst annotation: When designing APIs that are intended for analysis by package:record_use, consider using the @mustBeConst annotation on parameters. This annotation forces call sites to provide obviously constant arguments, which compilers are more likely to treat consistently across different optimization levels.
  • Warning for non-constant or dynamic calls: To improve the accuracy and reliability of package:record_use reports, consider adding a warning mechanism within the tool's analysis (e.g., in a “link hook”). This warning would trigger if any API calls are found with non-constant arguments, dynamic calls, or tear-offs. These constructs can lead to inconsistent reporting across compilers due to varying optimization strategies.
  • Aligning loading unit identifiers: When writing tests or verification logic that compares recordings across different compilers or compilation configurations, use the loadingUnitMapping parameter in semanticEquals to map/normalize the compiler-specific loading unit identifiers to a unified, consistent format.