Repository navigation
feat: Fickl performance experiments - #5135
Draft
shayanhabibi wants to merge 52 commits into
Draft
shayanhabibi wants to merge 52 commits into
shayanhabibi wants to merge 52 commits into
Conversation
…hat share a tag
A TypeScriptTaggedUnion case is matched only by its tag value, so two cases
cannot share one. TypeScript unions often do: arms with the same tag are told
apart by a second literal, by `in`, or by `typeof`.
[<CaseTest("<expr>")>] on a case adds a test, written in Emit syntax with $0 as
the tested value, ANDed with the tag test. Cases without it compile as before.
When the tested value could have side effects it is bound to a temporary, so it
is evaluated once. Being an attribute, it also works for bindings consumed from
a DLL.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CaseTest only takes effect on TypeScriptTaggedUnion cases. On any other union it was silently ignored; now testing such a case emits a warning. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The standalone compiler compiles against the committed metadata assembly, which did not yet contain CaseTestAttribute. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Since fable-compiler#3844, a TypeScriptTaggedUnion case whose single field is an interface compiles to `{ tag, Item }`, so TypeScript code reads `command.Item.fromIndex` rather than the documented `Take & { type: "take" }`. [<InlineInterface>] on a case, or on the union type for every case with fields, compiles the case as its interface with the tag added: - the case type is `Take & { type: "take" }`; - reading the field returns the union value; - construction copies the value with its prototype and sets the tag on the copy, so the caller's object is unchanged and class members keep working; the value is evaluated once; - matching and `IsTake` are unchanged. A case with InlineInterface must have exactly one field, whose type is an interface; otherwise construction and field reads report an error. Fable.AST: `UnionCase` gains `Attributes`, so Fable2Babel can read a case-level attribute when building the type annotation. Refs fable-compiler#5078: this is the compiler half; the docs page still needs updating. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LySb2Kjdc8qoY9Qoikr2u5
The standalone compiler compiles against the committed metadata assembly, which did not yet contain InlineInterfaceAttribute. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LySb2Kjdc8qoY9Qoikr2u5
Adds InlineInterface (fable-compiler#5113) next to CaseTest. The fable-metadata Fable.Core.dll is rebuilt from the merged Fable.Core so it carries both attributes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LySb2Kjdc8qoY9Qoikr2u5
Centralise Fickle package identity in src/Fickle.Branding.targets, imported from src/Directory.Build.targets, so upstream project files stay untouched: Fable.Cli -> fickle-tool (command: fickle), Fable.Compiler -> Fickle.Compiler, Fable.AST -> Fickle.AST, Fable.PublishUtils -> Fickle.PublishUtils. Namespaces and assembly names remain Fable.* to keep upstream merges cheap. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fable.Core stays identical to upstream so a project mixing Fickle code with existing Fable bindings resolves a single Fable.Core.dll. The fork-only attributes now live in the new Fickle.Core package (namespace Fickle.Core), which depends on Fable.Core and ships its sources under fable/ for Fable. The compiler accepts both Fable.Core.* and Fickle.Core.* names for these attributes, so they keep working if upstream adopts them in Fable.Core. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reports project cracking, FCS parse + type check, Fable per-file work (summed across threads, with average and max), total wall time, allocations and GC counts after each compilation. Lives in src/Fable.Cli/Timings.fs with single-line hooks in Main.fs and Entry.fs to keep upstream merges cheap. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
src/Fickle.Build declares upstream's build commands (fable-library, test, quicktest, standalone, worker-js, compiler-js, package) as typed Partas.Build commands with generated help, and runs each through Fable.Build's own Build.Main.main in-process, so src/Fable.Build stays untouched. Release commands that push Fable's package names are deliberately not exposed. Adds the Fickle `timings` command, the fickle-build.sh/.cmd launchers, and the first performance baseline in notes/perf-baseline.md. The Partas.Build feed is scoped to src/Fickle.Build/NuGet.config. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Run the build CLI directly with `dotnet run --project src/Fickle.Build -- <command>`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Builds the FCS fork (shayanhabibi/fickle-fsharp, branch fickle_slim) and copies its binaries into lib/fcs, cloning it next to the repo when missing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
lib/fcs now holds FSharp.Compiler.Service built for net10.0 from shayanhabibi/fickle-fsharp@da62eba (fickle_slim), with FSharp.Core built from the same source. Fable.Transforms moves from netstandard2.0 to net10.0 to consume it; all of its consumers already target net10. Same test results as before (compiler 133/133, JS 3159/3161 with the two known timezone failures). Wall time is unchanged within noise; allocations on tests/Js/Main drop from ~16 GB to ~11 GB. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Rebuild lib/fcs from fickle-fsharp 7687bf609. Allocation on tests/Js/Main drops from ~11.0 GB to ~8.7 GB and gen1 collections from ~44 to ~17; generated output is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Rebuild lib/fcs from fickle-fsharp ac0a7b8c5, which checks a cold project through the graph-based checker and keeps per-file states for watch mode. tests/Js/Main compiles in 3.0-4.1 s instead of 7.4 s, with identical output. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Rebuild lib/fcs from fickle-fsharp 7d83faa66. Compiling tests/Js/Main allocates 7.9 GB instead of 9.3 GB, with identical output. Also record that project cracking is already cached when --noCache is not passed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Rebuild lib/fcs from fickle-fsharp fff58bb81. In a tests/Js/Main watch session, an edit to Util.fs rechecks in 1.3 s instead of 3.6 s, with the same output as the sequential recheck. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
src/Fickle.Build is the build entry point. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
build.sh and build.bat are gone. Upstream's release commands are not exposed by Fickle.Build, so the ShipIt workflow calls Fable.Build directly. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
dateToStringWithOffset shifted the date by its offset into a plain Date, so day, month, year and hour then read the machine's local timezone and the D/d/F/f/G/g/M/Y and custom formats showed the wrong date when the shift crossed midnight locally. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Rename Fickle.Build, Fickle.Core and the branding targets to Fickl, and publish the CLI as the Fickl tool package (command `fickl`) at 0.1.0. The startup line now reports the Fickl version alongside the Fable base version; `--version` still prints the Fable version for tools that read it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GetOrAdd runs its factory outside the dictionary's lock, and the factory
reads every target dir chosen so far. Two files from different source
dirs with the same name, compiled at once, could both choose the same
dir and write the same output file ("being used by another process").
tests/Js/Main hits it with DllRef/Lib2.fs, about two runs in three with
DOTNET_TieredCompilation=0.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With FICKL_FCS_ACTIVITIES=<file.csv>, --timings also listens to FCS's "fsc" activity source and writes one row per activity (parse, check, import) with its file, start, duration and thread, as `fsc --times` does for the command-line compiler. It shows which files form the type-checking critical path inside Fable, including package sources. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fable type-checks the F# sources that packages ship on every cold compile, and on real projects that is often most of the type-checking time (Feliz alone: 3.9 s summed in the Fantomas online tools). A package version's sources never change, so after a successful compile Fickl precompiles them once, as `fable precompile` does (an assembly with their signatures, their inline bodies and their JS), into a per-user cache keyed by the compiler, options, referenced assemblies and the package sources' contents. Later compiles reference the assembly instead of checking the sources and copy the package JS into their own fable_modules, with identical output. On by default for JavaScript and TypeScript; --noPackageCache turns it off and FICKL_PACKAGE_CACHE moves it. Package sets FCS cannot emit as one assembly are recorded and skipped. Fantomas online tools, cold compile phase: 3.1-3.9 s -> 1.0-1.25 s. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Updates lib/fcs to fickle-fsharp 594a55f8b. Large binding files check much faster: Glutinum's TypeScript.fs 3.2-3.5 s -> 1.9 s, Ionide's compile 3.0-3.5 s -> 2.5 s. Output and warnings are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…sembly per package Sutil's packages declare both a module and a type Feliz.Util, which FCS type-checks but cannot emit as one assembly. When the single-assembly compile fails, compile each package as its own assembly referencing the ones before it, list the extra assemblies in groups.txt, and merge their precompiled info. Entity lookups for precompiled inline bodies search all of them, and Fable.Precompiled.<n> assemblies count as precompiled. Sutil compile phase on a cache hit: 4.8 s -> 2.1 s. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Python imports of cached files resolve through their out path in this project, as for JS. A Python library's __init__.py files are written on a hit, and the output type joins the key since Python emits __all__ for libraries. BEAM names modules by their path in the package, so it needs no import rewriting; its module-name clash check now covers cached sources. Package .fsi files no longer stop an entry from being saved. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Both stay comparable when other work loads the machine, unlike wall-clock times. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
dotnet pack now builds one tool package per runtime, each a composite ReadyToRun image of the compiler's assemblies, plus a portable IL package and the pointer package the .NET 10 SDK resolves. With TieredPGO off, hot R2R methods go straight to the optimised tier. Compile phase: JS suite 4.2 -> 3.2 s, Fantomas tools 3.7 -> 2.8 s, cache hits roughly halve. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…es sources The TargetFramework query now runs alongside the design-time build (Ionide cracking 7.6s -> 3.9s). Sources under paket-files are cached for JS/TS, and cache hits unify anonymous record types across the project and Fickl.Packages assemblies via the FCS fork hook (fickle-fsharp c0e7f8143). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Upstream merged CaseTest (fable-compiler#5076) with the attribute in Fable.Core. The fork already carried the compiler side with the attribute in Fickl.Core, so both attribute names stay recognised. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
References were cracked after the main design-time build, level by level, one dotnet msbuild process each. The references a project file states directly now start their design-time builds alongside the main one. A result is used when the referencing build asks for the same TargetFramework and restore rewrote none of the restore outputs it read; otherwise the reference is cracked as before. FICKL_CRACK_SPECULATION=0 turns it off. JS suite cracking 13.4 s -> 8.2 s (median of 5, interleaved), output identical. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nature The FCS fork now keeps a changed file's earlier check result in the state its dependents see when the file's signature fingerprint is unchanged, so the dependents are reused. JS suite, body edit in Util.Testing.fs: 57 files rechecked -> 0, compile 1089 -> 20 ms. Also fixes stale diagnostics and output in watch mode when a cycle checked part of the project and then all of it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A dotnet msbuild process started by project cracking kept running when the compiler was stopped while it ran, and one that never finished stayed forever. On Windows each one now joins a job object that kills it when the compiler's handle to the job closes, however the compiler exits; the processes it starts break away from the job, so MSBuild's reusable nodes keep their usual lifetime. Elsewhere they are killed when the compiler exits or is interrupted. Killing the compiler during Ionide's cracking left both msbuild children running with 0.5.0 and none with this change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…sses Each dotnet msbuild started while cracking, and the dotnet build of an excluded project, sent CLI telemetry on exit and waited for it. On a slow route that was about 9 s per process: the JS test suite took 27 s to crack, 1.9 s without it. DOTNET_CLI_TELEMETRY_OPTOUT=1 is now set for those processes unless the user set the variable; the user's --run commands are not affected. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 22, 2026
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Staging the full tree of changes for cherry picking.
Requires ncave/fsharp#20
Full performance exploration and rundown https://github.com/shayanhabibi/Fable/blob/9d10f9e4eb38b7db2bbf3ad5aa054b5d5f0e84bc/notes/perf-frontiers.md
Key areas of improvement:
Other fixes:
Warning
This contains unmerged PRs such as InlineInterface.