diff --git a/.claude/settings.json b/.claude/settings.json index 996bb9fef3..587373f1c6 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -1,8 +1,8 @@ { "permissions": { "allow": [ - "Bash(./build.sh fable-library *)", - "Bash(./build.sh test *)" + "Bash(dotnet run --project src/Fickl.Build -- fable-library *)", + "Bash(dotnet run --project src/Fickl.Build -- test *)" ], "deny": [ "Read(lib/fcs/**)", diff --git a/.github/actions/setup-node/action.yml b/.github/actions/setup-node/action.yml index 85be3be357..bdbd3d4700 100644 --- a/.github/actions/setup-node/action.yml +++ b/.github/actions/setup-node/action.yml @@ -8,7 +8,7 @@ runs: with: node-version-file: package.json cache: npm - # build.sh also installs in src/fable-compiler-js, src/fable-standalone and tests/React. + # The build also installs in src/fable-compiler-js, src/fable-standalone and tests/React. cache-dependency-path: | **/package-lock.json !**/node_modules/** diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3c65091a58..7b4ef21616 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -87,11 +87,11 @@ jobs: - name: Fable Tests - JavaScript (linux) if: matrix.platform == 'ubuntu-latest' - run: ./build.sh test javascript + run: dotnet run --project src/Fickl.Build -- test javascript - name: Fable Tests - JavaScript (Windows) if: matrix.platform == 'windows-latest' - run: .\build.bat test javascript + run: dotnet run --project src/Fickl.Build -- test javascript # Separate build job for TypeScript build-typescript: @@ -108,7 +108,7 @@ jobs: uses: ./.github/actions/setup-node - name: Fable Tests - TypeScript - run: ./build.sh test typescript + run: dotnet run --project src/Fickl.Build -- test typescript # - name: Fable Tests - TypeScript # run: dotnet fsi build.fsx test-ts @@ -128,7 +128,7 @@ jobs: uses: ./.github/actions/setup-node - name: Fable Tests - run: ./build.sh test integration + run: dotnet run --project src/Fickl.Build -- test integration # Separate build job for Standalone build-standalone: @@ -145,7 +145,7 @@ jobs: uses: ./.github/actions/setup-node - name: Fable Tests - run: ./build.sh test standalone + run: dotnet run --project src/Fickl.Build -- test standalone # Separate build job for Python since we use a test matrix (will run in parallell) build-python: @@ -179,7 +179,7 @@ jobs: - name: Fable Library - Python (linux) if: matrix.platform == 'ubuntu-latest' - run: ./build.sh fable-library --python + run: dotnet run --project src/Fickl.Build -- fable-library python # PEP 561 requires the marker inside the package directory. CopyStage filters # by extension, so it is easy to drop again; without it mypy refuses to read @@ -192,15 +192,15 @@ jobs: - name: Fable Library - Python (Windows) if: matrix.platform == 'windows-latest' - run: .\build.bat fable-library --python + run: dotnet run --project src/Fickl.Build -- fable-library python - name: Fable Tests - Python (linux) if: matrix.platform == 'ubuntu-latest' - run: ./build.sh test python + run: dotnet run --project src/Fickl.Build -- test python - name: Fable Tests - Python (Windows) if: matrix.platform == 'windows-latest' - run: .\build.bat test python + run: dotnet run --project src/Fickl.Build -- test python # Native/Rust-extension unit tests for the published fable-library package. # These live in src/fable-library-py/tests and exercise hand-written sources @@ -283,10 +283,10 @@ jobs: components: rustfmt - name: Fable Library - Rust - run: ./build.sh fable-library --rust + run: dotnet run --project src/Fickl.Build -- fable-library rust - name: Fable Tests - Rust - run: ./build.sh test rust --features "${{ matrix.features }}" + run: dotnet run --project src/Fickl.Build -- test rust --features "${{ matrix.features }}" # Separate build job for Dart build-dart: @@ -307,7 +307,7 @@ jobs: uses: dart-lang/setup-dart@v1 - name: Fable Tests - Dart - run: ./build.sh test dart + run: dotnet run --project src/Fickl.Build -- test dart # Separate build job for Beam (Erlang) build-beam: @@ -327,7 +327,7 @@ jobs: rebar3-version: "3" - name: Fable Tests - Beam - run: ./build.sh test beam + run: dotnet run --project src/Fickl.Build -- test beam # Separate job to run F# analyzers analyzers: diff --git a/.github/workflows/easybuild-shipit.yml b/.github/workflows/easybuild-shipit.yml index 1df8d71de9..23ce3dd4f5 100644 --- a/.github/workflows/easybuild-shipit.yml +++ b/.github/workflows/easybuild-shipit.yml @@ -31,7 +31,7 @@ jobs: uses: ./.github/actions/setup-dotnet # Run your tests before generating the changelog to make sure everything is working fine before creating a pull request. - name: Prepare for release - run: ./build.sh prepare-release + run: dotnet run --project src/Fable.Build -- prepare-release - name: ShipIt (Pull Request) # Configure add --pre-release below when we want to create a pre-release, e.g. --pre-release beta run: dotnet shipit @@ -80,7 +80,7 @@ jobs: - name: Build, and Release run: | - ./build.sh automated-release + dotnet run --project src/Fable.Build -- automated-release env: FABLE_NUGET_KEY: ${{ steps.login.outputs.NUGET_API_KEY }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index 39f4d30d07..ffca2b0411 100644 --- a/.github/workflows/publish-pypi.yml +++ b/.github/workflows/publish-pypi.yml @@ -36,7 +36,7 @@ jobs: - name: Install Maturin run: uv tool install maturin - name: Transpile F# to Python - run: ./build.sh fable-library --python + run: dotnet run --project src/Fickl.Build -- fable-library python - name: Install Dunamai run: uv tool install dunamai - name: Set version diff --git a/.github/workflows/python-type-checking.yml b/.github/workflows/python-type-checking.yml index f524ab542c..ee398cc591 100644 --- a/.github/workflows/python-type-checking.yml +++ b/.github/workflows/python-type-checking.yml @@ -42,7 +42,7 @@ jobs: pipx install maturin - name: Build Python (compile only) - run: ./build.sh test python --compile-only + run: dotnet run --project src/Fickl.Build -- test python --compile-only - name: Run Pyright and collect errors id: pyright diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 84802fef77..7ee2ea32a9 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -22,91 +22,71 @@ { "label": "Build fable-library - JavaScript", "type": "shell", + "command": "dotnet", "args": [ + "run", + "--project", + "src/Fickl.Build", + "--", "fable-library", - "--javascript" + "javascript" ], - "linux": { - "command": "${workspaceFolder}/build.sh" - }, - "osx": { - "command": "${workspaceFolder}/build.sh" - }, - "windows": { - "command": "${workspaceFolder}/build.cmd" - }, "problemMatcher": [] }, { "label": "Build fable-library - TypeScript", "type": "shell", + "command": "dotnet", "args": [ + "run", + "--project", + "src/Fickl.Build", + "--", "fable-library", - "--typescript" + "typescript" ], - "linux": { - "command": "${workspaceFolder}/build.sh" - }, - "osx": { - "command": "${workspaceFolder}/build.sh" - }, - "windows": { - "command": "${workspaceFolder}/build.cmd" - }, "problemMatcher": [] }, { "label": "Build fable-library - Python", "type": "shell", + "command": "dotnet", "args": [ + "run", + "--project", + "src/Fickl.Build", + "--", "fable-library", - "--python" + "python" ], - "linux": { - "command": "${workspaceFolder}/build.sh" - }, - "osx": { - "command": "${workspaceFolder}/build.sh" - }, - "windows": { - "command": "${workspaceFolder}/build.cmd" - }, "problemMatcher": [] }, { "label": "Build fable-library - Dart", "type": "shell", + "command": "dotnet", "args": [ + "run", + "--project", + "src/Fickl.Build", + "--", "fable-library", - "--dart" + "dart" ], - "linux": { - "command": "${workspaceFolder}/build.sh" - }, - "osx": { - "command": "${workspaceFolder}/build.sh" - }, - "windows": { - "command": "${workspaceFolder}/build.cmd" - }, "problemMatcher": [] }, { "label": "Build fable-library - Rust", "type": "shell", + "command": "dotnet", "args": [ + "run", + "--project", + "src/Fickl.Build", + "--", "fable-library", - "--rust" + "rust" ], - "linux": { - "command": "${workspaceFolder}/build.sh" - }, - "osx": { - "command": "${workspaceFolder}/build.sh" - }, - "windows": { - "command": "${workspaceFolder}/build.cmd" - }, "problemMatcher": [] }, { diff --git a/AGENTS.md b/AGENTS.md index 53a4b0ac4d..f065b7b3f6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -8,30 +8,30 @@ Fable is an F# to multi-language compiler that transpiles F# code to JavaScript, ## Build Commands -The build system is implemented in F# at `src/Fable.Build/`. All commands go through `build.sh` (or `build.bat` on Windows): +The build system is implemented in F# at `src/Fable.Build/`. All commands go through `src/Fickl.Build/`, which runs the same Fable.Build handlers: ```bash -./build.sh # Show all available commands +dotnet run --project src/Fickl.Build -- --help # Show all available commands # Build runtime libraries -./build.sh fable-library --javascript # Also: --typescript, --python, --dart, --rust, --beam +dotnet run --project src/Fickl.Build -- fable-library javascript # Also: typescript, python, dart, rust, beam # Run tests (targets: javascript, typescript, python, dart, rust, beam, integration) -./build.sh test javascript -./build.sh test javascript --watch # Watch mode (recompile on changes) -./build.sh test javascript --force-fable-library # Force fable-library rebuild (needed if you modified Fable compiler to fix the generation of `src/fable-library-*/**/*.fs` files) +dotnet run --project src/Fickl.Build -- test javascript +dotnet run --project src/Fickl.Build -- test javascript --watch # Watch mode (recompile on changes) +dotnet run --project src/Fickl.Build -- test javascript --force-fable-library # Force fable-library rebuild (needed if you modified Fable compiler to fix the generation of `src/fable-library-*/**/*.fs` files) # Python-specific options -./build.sh test python --skip-fable-library-core # Skip slow Rust extension + .pyi rebuild (Python only) -./build.sh test python --type-check # Run Pyright type checking on Python output +dotnet run --project src/Fickl.Build -- test python --skip-fable-library-core # Skip slow Rust extension + .pyi rebuild (Python only) +dotnet run --project src/Fickl.Build -- test python --type-check # Run Pyright type checking on Python output # Quick iteration (test on a minimal project) -./build.sh quicktest javascript # Also: typescript, python, dart, rust, beam +dotnet run --project src/Fickl.Build -- quicktest javascript # Also: typescript, python, dart, rust, beam ``` In most cases, you should not need to use `--force-fable-library`. Only use it if you modified compiler code that affects how F# files used in `src/fable-library-*/` are generated (e.g., changes to `FSharp2Fable.fs` that affect how F# AST is transformed into Fable AST for library files). -When running Python tooling directly with `uv` (e.g. `pytest`, `pyright`, `maturin`, ad-hoc scripts) instead of through `build.sh`, always pass `--frozen` (e.g. `uv run --frozen pytest`). Plain `uv run` silently resolves and rewrites `uv.lock` as a side effect of just running a command. If a command fails because the lock is out of date, stop and ask rather than letting `uv` update it. +When running Python tooling directly with `uv` (e.g. `pytest`, `pyright`, `maturin`, ad-hoc scripts) instead of through the build, always pass `--frozen` (e.g. `uv run --frozen pytest`). Plain `uv run` silently resolves and rewrites `uv.lock` as a side effect of just running a command. If a command fails because the lock is out of date, stop and ask rather than letting `uv` update it. Build output goes to `temp/`: transpiled runtime libraries in `temp/fable-library-/` and test output in `temp/tests//` (e.g., `temp/fable-library-beam/` and `temp/tests/beam/`). @@ -100,7 +100,7 @@ Be careful when modifying shared files like `FableTransforms.fs`, `Transforms.Ut All code in `fable-library-py` must be fully type-annotated using Python 3.13-compatible syntax, including PEP 695 type parameters. Avoid `Any`, `cast()`, and `# type: ignore` — these require justification. Keep functions small and simple: extract helpers and use early returns. The Rust extension modules (`fable-library-core`) exist to provide correct .NET semantics for numerics, arrays, etc. — not for performance (the FFI crossing is expensive). -All transpiled Python code is type-checked with Pyright at standard settings (`./build.sh test python --type-check`). Do not introduce type-checking regressions without justification. +All transpiled Python code is type-checked with Pyright at standard settings (`dotnet run --project src/Fickl.Build -- test python --type-check`). Do not introduce type-checking regressions without justification. ## Development Workflow diff --git a/README.md b/README.md index 32cafb98b5..530c5d4660 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ Make sure the following **requirements** are installed in your system: ### Build -Run `./build.sh` or `./build.cmd` to see the build options. +Run `dotnet run --project src/Fickl.Build -- --help` to see the build options. When using VSCode, you can also run the build tasks from the command palette (Ctrl+Shift+P) by typing `Run Task` and selecting the task you want to run. diff --git a/build.bat b/build.bat deleted file mode 100644 index 1b446493e8..0000000000 --- a/build.bat +++ /dev/null @@ -1,4 +0,0 @@ -@echo off - -dotnet tool restore -dotnet run --project src/Fable.Build/Fable.Build.fsproj -- %* diff --git a/build.sh b/build.sh deleted file mode 100755 index 807a756d27..0000000000 --- a/build.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/sh -x - -dotnet tool restore -dotnet run --project src/Fable.Build/Fable.Build.fsproj -- $@ diff --git a/lib/fcs/FSharp.Compiler.Service.deps.json b/lib/fcs/FSharp.Compiler.Service.deps.json index 51d6ca1ab2..4a91fd8697 100644 --- a/lib/fcs/FSharp.Compiler.Service.deps.json +++ b/lib/fcs/FSharp.Compiler.Service.deps.json @@ -1,342 +1,53 @@ { "runtimeTarget": { - "name": ".NETStandard,Version=v2.0/", + "name": ".NETCoreApp,Version=v10.0", "signature": "" }, "compilationOptions": {}, "targets": { - ".NETStandard,Version=v2.0": {}, - ".NETStandard,Version=v2.0/": { - "FSharp.Compiler.Service/43.11.200-dev": { + ".NETCoreApp,Version=v10.0": { + "FSharp.Compiler.Service/43.11.200": { "dependencies": { - "FSharp.Core": "10.0.200-dev", - "FSharp.DependencyManager.Nuget": "10.0.200-dev", - "System.Buffers": "4.6.0", - "System.Collections.Immutable": "9.0.0", - "System.Diagnostics.DiagnosticSource": "9.0.0", - "System.Memory": "4.6.0", - "System.Reflection.Emit": "4.7.0", - "System.Reflection.Metadata": "9.0.0", - "System.Runtime.CompilerServices.Unsafe": "6.1.0" + "FSharp.Core": "10.0.200", + "FSharp.DependencyManager.Nuget": "10.0.200" }, "runtime": { "FSharp.Compiler.Service.dll": {} - }, - "resources": { - "cs/FSharp.Compiler.Service.resources.dll": { - "locale": "cs" - }, - "de/FSharp.Compiler.Service.resources.dll": { - "locale": "de" - }, - "es/FSharp.Compiler.Service.resources.dll": { - "locale": "es" - }, - "fr/FSharp.Compiler.Service.resources.dll": { - "locale": "fr" - }, - "it/FSharp.Compiler.Service.resources.dll": { - "locale": "it" - }, - "ja/FSharp.Compiler.Service.resources.dll": { - "locale": "ja" - }, - "ko/FSharp.Compiler.Service.resources.dll": { - "locale": "ko" - }, - "pl/FSharp.Compiler.Service.resources.dll": { - "locale": "pl" - }, - "pt-BR/FSharp.Compiler.Service.resources.dll": { - "locale": "pt-BR" - }, - "ru/FSharp.Compiler.Service.resources.dll": { - "locale": "ru" - }, - "tr/FSharp.Compiler.Service.resources.dll": { - "locale": "tr" - }, - "zh-Hans/FSharp.Compiler.Service.resources.dll": { - "locale": "zh-Hans" - }, - "zh-Hant/FSharp.Compiler.Service.resources.dll": { - "locale": "zh-Hant" - } - } - }, - "System.Buffers/4.6.0": { - "runtime": { - "lib/netstandard2.0/System.Buffers.dll": { - "assemblyVersion": "4.0.2.0", - "fileVersion": "4.600.24.56208" - } - } - }, - "System.Collections.Immutable/9.0.0": { - "dependencies": { - "System.Memory": "4.6.0", - "System.Runtime.CompilerServices.Unsafe": "6.1.0" - }, - "runtime": { - "lib/netstandard2.0/System.Collections.Immutable.dll": { - "assemblyVersion": "9.0.0.0", - "fileVersion": "9.0.24.52809" - } } }, - "System.Diagnostics.DiagnosticSource/9.0.0": { - "dependencies": { - "System.Memory": "4.6.0", - "System.Runtime.CompilerServices.Unsafe": "6.1.0" - }, - "runtime": { - "lib/netstandard2.0/System.Diagnostics.DiagnosticSource.dll": { - "assemblyVersion": "9.0.0.0", - "fileVersion": "9.0.24.52809" - } - } - }, - "System.Memory/4.6.0": { - "dependencies": { - "System.Buffers": "4.6.0", - "System.Numerics.Vectors": "4.6.0", - "System.Runtime.CompilerServices.Unsafe": "6.1.0" - }, - "runtime": { - "lib/netstandard2.0/System.Memory.dll": { - "assemblyVersion": "4.0.2.0", - "fileVersion": "4.600.24.56208" - } - } - }, - "System.Numerics.Vectors/4.6.0": { - "runtime": { - "lib/netstandard2.0/System.Numerics.Vectors.dll": { - "assemblyVersion": "4.1.3.0", - "fileVersion": "4.600.24.56208" - } - } - }, - "System.Reflection.Emit/4.7.0": { - "dependencies": { - "System.Reflection.Emit.ILGeneration": "4.7.0" - }, - "runtime": { - "lib/netstandard2.0/System.Reflection.Emit.dll": { - "assemblyVersion": "4.0.0.0", - "fileVersion": "4.700.19.56404" - } - } - }, - "System.Reflection.Emit.ILGeneration/4.7.0": { - "runtime": { - "lib/netstandard2.0/System.Reflection.Emit.ILGeneration.dll": { - "assemblyVersion": "4.0.0.0", - "fileVersion": "4.700.19.56404" - } - } - }, - "System.Reflection.Metadata/9.0.0": { - "dependencies": { - "System.Collections.Immutable": "9.0.0", - "System.Memory": "4.6.0" - }, - "runtime": { - "lib/netstandard2.0/System.Reflection.Metadata.dll": { - "assemblyVersion": "9.0.0.0", - "fileVersion": "9.0.24.52809" - } - } - }, - "System.Runtime.CompilerServices.Unsafe/6.1.0": { - "runtime": { - "lib/netstandard2.0/System.Runtime.CompilerServices.Unsafe.dll": { - "assemblyVersion": "6.0.0.0", - "fileVersion": "6.100.24.56208" - } - } - }, - "FSharp.Core/10.0.200-dev": { + "FSharp.Core/10.0.200": { "runtime": { "FSharp.Core.dll": { "assemblyVersion": "10.0.0.0", - "fileVersion": "42.42.42.42424" - } - }, - "resources": { - "zh-Hans/FSharp.Core.resources.dll": { - "locale": "zh-Hans" - }, - "pt-BR/FSharp.Core.resources.dll": { - "locale": "pt-BR" - }, - "es/FSharp.Core.resources.dll": { - "locale": "es" - }, - "fr/FSharp.Core.resources.dll": { - "locale": "fr" - }, - "zh-Hant/FSharp.Core.resources.dll": { - "locale": "zh-Hant" - }, - "de/FSharp.Core.resources.dll": { - "locale": "de" - }, - "ja/FSharp.Core.resources.dll": { - "locale": "ja" - }, - "cs/FSharp.Core.resources.dll": { - "locale": "cs" - }, - "tr/FSharp.Core.resources.dll": { - "locale": "tr" - }, - "it/FSharp.Core.resources.dll": { - "locale": "it" - }, - "pl/FSharp.Core.resources.dll": { - "locale": "pl" - }, - "ru/FSharp.Core.resources.dll": { - "locale": "ru" - }, - "ko/FSharp.Core.resources.dll": { - "locale": "ko" + "fileVersion": "10.0.0.0" } } }, - "FSharp.DependencyManager.Nuget/10.0.200-dev": { + "FSharp.DependencyManager.Nuget/10.0.200": { "dependencies": { - "FSharp.Core": "10.0.200-dev", - "System.Reflection.Emit": "4.7.0", - "System.Reflection.Metadata": "9.0.0", - "System.Runtime.CompilerServices.Unsafe": "6.1.0" + "FSharp.Core": "10.0.200" }, "runtime": { "FSharp.DependencyManager.Nuget.dll": { "assemblyVersion": "10.0.200.0", - "fileVersion": "42.42.42.42424" - } - }, - "resources": { - "zh-Hans/FSharp.DependencyManager.Nuget.resources.dll": { - "locale": "zh-Hans" - }, - "pt-BR/FSharp.DependencyManager.Nuget.resources.dll": { - "locale": "pt-BR" - }, - "es/FSharp.DependencyManager.Nuget.resources.dll": { - "locale": "es" - }, - "fr/FSharp.DependencyManager.Nuget.resources.dll": { - "locale": "fr" - }, - "zh-Hant/FSharp.DependencyManager.Nuget.resources.dll": { - "locale": "zh-Hant" - }, - "de/FSharp.DependencyManager.Nuget.resources.dll": { - "locale": "de" - }, - "ja/FSharp.DependencyManager.Nuget.resources.dll": { - "locale": "ja" - }, - "cs/FSharp.DependencyManager.Nuget.resources.dll": { - "locale": "cs" - }, - "tr/FSharp.DependencyManager.Nuget.resources.dll": { - "locale": "tr" - }, - "it/FSharp.DependencyManager.Nuget.resources.dll": { - "locale": "it" - }, - "pl/FSharp.DependencyManager.Nuget.resources.dll": { - "locale": "pl" - }, - "ru/FSharp.DependencyManager.Nuget.resources.dll": { - "locale": "ru" - }, - "ko/FSharp.DependencyManager.Nuget.resources.dll": { - "locale": "ko" + "fileVersion": "10.0.200.0" } } } } }, "libraries": { - "FSharp.Compiler.Service/43.11.200-dev": { + "FSharp.Compiler.Service/43.11.200": { "type": "project", "serviceable": false, "sha512": "" }, - "System.Buffers/4.6.0": { - "type": "package", - "serviceable": true, - "sha512": "sha512-lN6tZi7Q46zFzAbRYXTIvfXcyvQQgxnY7Xm6C6xQ9784dEL1amjM6S6Iw4ZpsvesAKnRVsM4scrDQaDqSClkjA==", - "path": "system.buffers/4.6.0", - "hashPath": "system.buffers.4.6.0.nupkg.sha512" - }, - "System.Collections.Immutable/9.0.0": { - "type": "package", - "serviceable": true, - "sha512": "sha512-QhkXUl2gNrQtvPmtBTQHb0YsUrDiDQ2QS09YbtTTiSjGcf7NBqtYbrG/BE06zcBPCKEwQGzIv13IVdXNOSub2w==", - "path": "system.collections.immutable/9.0.0", - "hashPath": "system.collections.immutable.9.0.0.nupkg.sha512" - }, - "System.Diagnostics.DiagnosticSource/9.0.0": { - "type": "package", - "serviceable": true, - "sha512": "sha512-ddppcFpnbohLWdYKr/ZeLZHmmI+DXFgZ3Snq+/E7SwcdW4UnvxmaugkwGywvGVWkHPGCSZjCP+MLzu23AL5SDw==", - "path": "system.diagnostics.diagnosticsource/9.0.0", - "hashPath": "system.diagnostics.diagnosticsource.9.0.0.nupkg.sha512" - }, - "System.Memory/4.6.0": { - "type": "package", - "serviceable": true, - "sha512": "sha512-OEkbBQoklHngJ8UD8ez2AERSk2g+/qpAaSWWCBFbpH727HxDq5ydVkuncBaKcKfwRqXGWx64dS6G1SUScMsitg==", - "path": "system.memory/4.6.0", - "hashPath": "system.memory.4.6.0.nupkg.sha512" - }, - "System.Numerics.Vectors/4.6.0": { - "type": "package", - "serviceable": true, - "sha512": "sha512-t+SoieZsRuEyiw/J+qXUbolyO219tKQQI0+2/YI+Qv7YdGValA6WiuokrNKqjrTNsy5ABWU11bdKOzUdheteXg==", - "path": "system.numerics.vectors/4.6.0", - "hashPath": "system.numerics.vectors.4.6.0.nupkg.sha512" - }, - "System.Reflection.Emit/4.7.0": { - "type": "package", - "serviceable": true, - "sha512": "sha512-VR4kk8XLKebQ4MZuKuIni/7oh+QGFmZW3qORd1GvBq/8026OpW501SzT/oypwiQl4TvT8ErnReh/NzY9u+C6wQ==", - "path": "system.reflection.emit/4.7.0", - "hashPath": "system.reflection.emit.4.7.0.nupkg.sha512" - }, - "System.Reflection.Emit.ILGeneration/4.7.0": { - "type": "package", - "serviceable": true, - "sha512": "sha512-AucBYo3DSI0IDxdUjKksBcQJXPHyoPyrCXYURW1WDsLI4M65Ar/goSHjdnHOAY9MiYDNKqDlIgaYm+zL2hA1KA==", - "path": "system.reflection.emit.ilgeneration/4.7.0", - "hashPath": "system.reflection.emit.ilgeneration.4.7.0.nupkg.sha512" - }, - "System.Reflection.Metadata/9.0.0": { - "type": "package", - "serviceable": true, - "sha512": "sha512-ANiqLu3DxW9kol/hMmTWbt3414t9ftdIuiIU7j80okq2YzAueo120M442xk1kDJWtmZTqWQn7wHDvMRipVOEOQ==", - "path": "system.reflection.metadata/9.0.0", - "hashPath": "system.reflection.metadata.9.0.0.nupkg.sha512" - }, - "System.Runtime.CompilerServices.Unsafe/6.1.0": { - "type": "package", - "serviceable": true, - "sha512": "sha512-5o/HZxx6RVqYlhKSq8/zronDkALJZUT2Vz0hx43f0gwe8mwlM0y2nYlqdBwLMzr262Bwvpikeb/yEwkAa5PADg==", - "path": "system.runtime.compilerservices.unsafe/6.1.0", - "hashPath": "system.runtime.compilerservices.unsafe.6.1.0.nupkg.sha512" - }, - "FSharp.Core/10.0.200-dev": { + "FSharp.Core/10.0.200": { "type": "project", "serviceable": false, "sha512": "" }, - "FSharp.DependencyManager.Nuget/10.0.200-dev": { + "FSharp.DependencyManager.Nuget/10.0.200": { "type": "project", "serviceable": false, "sha512": "" diff --git a/lib/fcs/FSharp.Compiler.Service.dll b/lib/fcs/FSharp.Compiler.Service.dll index e4b4794a65..d071bafa2c 100644 Binary files a/lib/fcs/FSharp.Compiler.Service.dll and b/lib/fcs/FSharp.Compiler.Service.dll differ diff --git a/lib/fcs/FSharp.Compiler.Service.xml b/lib/fcs/FSharp.Compiler.Service.xml index c2c92abc9c..f9dd6b96d6 100644 --- a/lib/fcs/FSharp.Compiler.Service.xml +++ b/lib/fcs/FSharp.Compiler.Service.xml @@ -10772,13 +10772,13 @@ Stopped due to error\n - (Originally from Interactive/FSIstrings.txt:2) + (Originally from Interactive\FSIstrings.txt:2) Prevents references from being locked by the F# Interactive process (%s by default) - (Originally from Interactive/FSIstrings.txt:56) + (Originally from Interactive\FSIstrings.txt:56) @@ -10794,349 +10794,349 @@ Use the given file on startup as initial input - (Originally from Interactive/FSIstrings.txt:11) + (Originally from Interactive\FSIstrings.txt:11) Usage: %s <options> [script.fsx [<arguments>]] - (Originally from Interactive/FSIstrings.txt:3) + (Originally from Interactive\FSIstrings.txt:3) - Unexpected ThreadAbortException (Ctrl-C) during event handling: Trying to restart... - (Originally from Interactive/FSIstrings.txt:51) + (Originally from Interactive\FSIstrings.txt:51) --> Timing now on - (Originally from Interactive/FSIstrings.txt:49) + (Originally from Interactive\FSIstrings.txt:49) --> Timing now off - (Originally from Interactive/FSIstrings.txt:50) + (Originally from Interactive\FSIstrings.txt:50) Real: %s, CPU: %s, GC %s - (Originally from Interactive/FSIstrings.txt:25) + (Originally from Interactive\FSIstrings.txt:25) gen - (Originally from Interactive/FSIstrings.txt:26) + (Originally from Interactive\FSIstrings.txt:26) Treat remaining arguments as command line arguments, accessed using fsi.CommandLineArgs - (Originally from Interactive/FSIstrings.txt:13) + (Originally from Interactive\FSIstrings.txt:13) Support TAB completion in console (%s by default) - (Originally from Interactive/FSIstrings.txt:18) + (Originally from Interactive\FSIstrings.txt:18) Suppress fsi writing to stdout - (Originally from Interactive/FSIstrings.txt:17) + (Originally from Interactive\FSIstrings.txt:17) F# Interactive for F# %s - (Originally from Interactive/FSIstrings.txt:55) + (Originally from Interactive\FSIstrings.txt:55) Microsoft (R) F# Interactive version %s - (Originally from Interactive/FSIstrings.txt:54) + (Originally from Interactive\FSIstrings.txt:54) Operation failed. The error text has been printed in the error stream. To return the corresponding FSharpDiagnostic use the EvalInteractionNonThrowing, EvalScriptNonThrowing or EvalExpressionNonThrowing - (Originally from Interactive/FSIstrings.txt:58) + (Originally from Interactive\FSIstrings.txt:58) Operation could not be completed due to earlier error - (Originally from Interactive/FSIstrings.txt:57) + (Originally from Interactive\FSIstrings.txt:57) Emit multiple assemblies (%s by default) - (Originally from Interactive/FSIstrings.txt:59) + (Originally from Interactive\FSIstrings.txt:59) - MISCELLANEOUS - - (Originally from Interactive/FSIstrings.txt:8) + (Originally from Interactive\FSIstrings.txt:8) Loading - (Originally from Interactive/FSIstrings.txt:40) + (Originally from Interactive\FSIstrings.txt:40) #load the given file on startup - (Originally from Interactive/FSIstrings.txt:12) + (Originally from Interactive\FSIstrings.txt:12) Warning: line too long, ignoring some characters\n - (Originally from Interactive/FSIstrings.txt:24) + (Originally from Interactive\FSIstrings.txt:24) - LANGUAGE - - (Originally from Interactive/FSIstrings.txt:7) + (Originally from Interactive\FSIstrings.txt:7) '%s' is not a valid assembly name - (Originally from Interactive/FSIstrings.txt:22) + (Originally from Interactive\FSIstrings.txt:22) See '%s' for options - (Originally from Interactive/FSIstrings.txt:39) + (Originally from Interactive\FSIstrings.txt:39) F# Interactive command line options: - (Originally from Interactive/FSIstrings.txt:38) + (Originally from Interactive\FSIstrings.txt:38) F# Interactive directives: - (Originally from Interactive/FSIstrings.txt:28) + (Originally from Interactive\FSIstrings.txt:28) Toggle timing on/off - (Originally from Interactive/FSIstrings.txt:33) + (Originally from Interactive\FSIstrings.txt:33) Reference (dynamically load) the given DLL - (Originally from Interactive/FSIstrings.txt:29) + (Originally from Interactive\FSIstrings.txt:29) Exit - (Originally from Interactive/FSIstrings.txt:36) + (Originally from Interactive\FSIstrings.txt:36) Load the given file(s) as if compiled and referenced - (Originally from Interactive/FSIstrings.txt:32) + (Originally from Interactive\FSIstrings.txt:32) Display documentation for an identifier, e.g. #help \"List.map\";; - (Originally from Interactive/FSIstrings.txt:35) + (Originally from Interactive\FSIstrings.txt:35) Display help - (Originally from Interactive/FSIstrings.txt:34) + (Originally from Interactive\FSIstrings.txt:34) Clear screen - (Originally from Interactive/FSIstrings.txt:37) + (Originally from Interactive\FSIstrings.txt:37) Add the given search path for referenced DLLs - (Originally from Interactive/FSIstrings.txt:30) + (Originally from Interactive\FSIstrings.txt:30) Include package source uri when searching for packages - (Originally from Interactive/FSIstrings.txt:31) + (Originally from Interactive\FSIstrings.txt:31) \n- Interrupt\n - (Originally from Interactive/FSIstrings.txt:41) + (Originally from Interactive\FSIstrings.txt:41) - INPUT FILES - - (Originally from Interactive/FSIstrings.txt:4) + (Originally from Interactive\FSIstrings.txt:4) Display this usage message (Short form: -?) - (Originally from Interactive/FSIstrings.txt:14) + (Originally from Interactive\FSIstrings.txt:14) Execute interactions on a Windows Forms event loop (%s by default) - (Originally from Interactive/FSIstrings.txt:16) + (Originally from Interactive\FSIstrings.txt:16) Failed to resolve assembly '%s' - (Originally from Interactive/FSIstrings.txt:52) + (Originally from Interactive\FSIstrings.txt:52) \n- Exit...\n - (Originally from Interactive/FSIstrings.txt:42) + (Originally from Interactive\FSIstrings.txt:42) Exit fsi after loading the files or running the .fsx script given on the command line - (Originally from Interactive/FSIstrings.txt:15) + (Originally from Interactive\FSIstrings.txt:15) Exception raised when starting remoting server.\n%s - (Originally from Interactive/FSIstrings.txt:10) + (Originally from Interactive\FSIstrings.txt:10) \n\nException raised during pretty printing.\nPlease report this so it can be fixed.\nTrace: %s\n - (Originally from Interactive/FSIstrings.txt:27) + (Originally from Interactive\FSIstrings.txt:27) - ERRORS AND WARNINGS - - (Originally from Interactive/FSIstrings.txt:6) + (Originally from Interactive\FSIstrings.txt:6) Functions with [<EntryPoint>] are not invoked in FSI. '%s' was not invoked. Execute '%s <args>' in order to invoke '%s' with the appropriate string array of command line arguments. - (Originally from Interactive/FSIstrings.txt:60) + (Originally from Interactive\FSIstrings.txt:60) Emit debug information in quotations (%s by default) - (Originally from Interactive/FSIstrings.txt:19) + (Originally from Interactive\FSIstrings.txt:19) Directory '%s' doesn't exist - (Originally from Interactive/FSIstrings.txt:23) + (Originally from Interactive\FSIstrings.txt:23) --> Referenced '%s' (an assembly with a different timestamp has already been referenced from this location, reset fsi to load the updated assembly) - (Originally from Interactive/FSIstrings.txt:47) + (Originally from Interactive\FSIstrings.txt:47) --> Referenced '%s' (file may be locked by F# Interactive process) - (Originally from Interactive/FSIstrings.txt:46) + (Originally from Interactive\FSIstrings.txt:46) --> Referenced '%s' - (Originally from Interactive/FSIstrings.txt:45) + (Originally from Interactive\FSIstrings.txt:45) --> Added '%s' to library include path - (Originally from Interactive/FSIstrings.txt:48) + (Originally from Interactive\FSIstrings.txt:48) See https://learn.microsoft.com/dotnet/fsharp/language-reference/fsharp-interactive-options for more details. - (Originally from Interactive/FSIstrings.txt:61) + (Originally from Interactive\FSIstrings.txt:61) Failed to install ctrl-c handler - Ctrl-C handling will not be available. Error was:\n\t%s - (Originally from Interactive/FSIstrings.txt:44) + (Originally from Interactive\FSIstrings.txt:44) A problem occurred starting the F# Interactive process. This may be due to a known problem with background process console support for Unicode-enabled applications on some Windows systems. Try selecting Tools->Options->F# Interactive for Visual Studio and enter '--fsi-server-no-unicode'. - (Originally from Interactive/FSIstrings.txt:21) + (Originally from Interactive\FSIstrings.txt:21) - CODE GENERATION - - (Originally from Interactive/FSIstrings.txt:5) + (Originally from Interactive\FSIstrings.txt:5) Binding session to '%s'... - (Originally from Interactive/FSIstrings.txt:53) + (Originally from Interactive\FSIstrings.txt:53) For help type #help;; - (Originally from Interactive/FSIstrings.txt:20) + (Originally from Interactive\FSIstrings.txt:20) - ADVANCED - - (Originally from Interactive/FSIstrings.txt:9) + (Originally from Interactive\FSIstrings.txt:9) - Aborting main thread... - (Originally from Interactive/FSIstrings.txt:43) + (Originally from Interactive\FSIstrings.txt:43) @@ -11152,13 +11152,13 @@ %s for F# %s - (Originally from Facilities/UtilsStrings.txt:2) + (Originally from Facilities\UtilsStrings.txt:2) Microsoft (R) F# Compiler version %s - (Originally from Facilities/UtilsStrings.txt:1) + (Originally from Facilities\UtilsStrings.txt:1) @@ -21560,6 +21560,16 @@ Get the signature for the value's XML documentation For dereferencing in the middle of a pattern + + + Equality on the assemblies of two anonymous record types + + + + + Assemblies whose anonymous record types are one, as if compiled together (none unless set) + + Equality on CCU references, implemented as reference equality except when unresolved @@ -25872,6 +25882,12 @@ Get the signature for the value's XML documentation Determine if a type has a static abstract method with the given name somewhere in its hierarchy + + + Tests for a visible intrinsic static abstract method, including inherited methods. + Override and hiding checks are skipped when the raw hierarchy has no candidate slots. + + Get the trait infos for a type variable (empty for everything else) @@ -26904,6 +26920,11 @@ Get the signature for the value's XML documentation Get the available methods of a type (both declared and inherited) + + + Get the intrinsic methods of a type (both declared and inherited), without extension methods + + Get all the available properties of a type (only extension) @@ -31762,6 +31783,12 @@ Get the signature for the value's XML documentation Check a single input and finish the checking + + + Check a closed set of inputs in parallel over their dependency graph, leaving the check of each file to + 'checkFile', and return each file's result and the state after it, in file order. + + Check a closed set of inputs @@ -39170,6 +39197,13 @@ Get the signature for the value's XML documentation A default implementation of the 'fsi' object, used by GetDefaultConfiguration() + + + Treats the anonymous record types of the assemblies `isUnified` accepts as one, as if compiled together, for + every checker in the process: for sources checked as assemblies of their own that a compile of the project + would check in its assembly. + + Parses and checks the whole project, good for compilers (Fable etc.) @@ -39201,11 +39235,86 @@ Get the signature for the value's XML documentation Clears parse and typecheck caches. + + + The file was checked, and its dependents see the new result. + + + + + The file changed but its signature did not, and none of the files it depends on was replaced. The earlier + finisher stays, so the file's dependents can be reused; the new implementation is reported. + + + + + The file is unchanged, and none of the files it depends on, now or in the earlier check, was replaced. + + + + + How a file's result was obtained in a check over the dependency graph. + + + + + The implementation to report, from the latest check of the file. + + + + + Adds the file's signature to the state its dependents are checked in. Kept from an earlier check while later + checks of the file give the same signature, so the dependents' results stay valid. + + + + + The files this one depends on, with the generations of the finishers it was checked against. + + + + + Identifies the finisher: a new one gets a new generation. + + + + + The result of checking one file over the dependency graph, kept for reuse while + neither the file nor anything it depends on changes. + + Errors grouped by file, sorted by line, column + + + Type checks the files in parallel where the file dependency graph allows. A file checked by an earlier call is + reused while neither it nor anything it depends on has changed, so after an edit only the edited file and its + dependents are checked again. Fills the check cache with the same per-file entries a sequential check would + leave. The subscriber gets the files in project order, each as soon as it and all files before it are checked, + because Fable expects every earlier file to be known, not only the file's dependencies. + + + + + Whether a changed file whose signature is the same keeps its dependents' results (FICKL_FCS_SIGNATURE_CUTOFF=0 turns it off). + + + + + The last generation given to a graph check finisher; 0 is never given. + + + + + What the files that depend on a file see of it: the types, values, members and attributes its signature declares, + without ranges or stamps, and with type parameters numbered by position. Equal fingerprints mean the dependents + would check the same against either signature. Anything it cannot describe exactly gives None, so the file + always counts as changed. + + Get the Reference Assemblies directory for the .NET Framework (on Windows) diff --git a/lib/fcs/FSharp.Core.dll b/lib/fcs/FSharp.Core.dll index d35bdcc9fd..c8311dfc81 100644 Binary files a/lib/fcs/FSharp.Core.dll and b/lib/fcs/FSharp.Core.dll differ diff --git a/lib/fcs/FSharp.Core.xml b/lib/fcs/FSharp.Core.xml index 96783c2dc4..9a5d43ec19 100644 --- a/lib/fcs/FSharp.Core.xml +++ b/lib/fcs/FSharp.Core.xml @@ -39,6 +39,16 @@ An abbreviation for the CLI type + + Creates a list with the specified items. + + The items to store in the list. + + A list containing the specified items. + + + Contains methods for compiler use related to lists. + The type of immutable singly-linked lists. @@ -305,6 +315,16 @@ All members of this class are thread-safe and may be used concurrently from multiple threads. + + Creates a set with the specified items. + + The items to store in the set. + + A set containing the specified items. + + + Contains methods for compiler use related to sets. + Returns a new set with the elements of the second set removed from the first. @@ -17532,6 +17552,11 @@ Specifies the iterative execution of a unit of task code. + + + Specifies a unit of task code which binds to the resource implementing IAsyncDisposable and disposes it asynchronously + + Specifies a unit of task code which executed using try/with semantics diff --git a/lib/fcs/FSharp.DependencyManager.Nuget.dll b/lib/fcs/FSharp.DependencyManager.Nuget.dll index 4af001e702..c13807de3e 100644 Binary files a/lib/fcs/FSharp.DependencyManager.Nuget.dll and b/lib/fcs/FSharp.DependencyManager.Nuget.dll differ diff --git a/lib/fcs/FSharp.DependencyManager.Nuget.xml b/lib/fcs/FSharp.DependencyManager.Nuget.xml index 358b3b63ba..e11ff0be64 100644 --- a/lib/fcs/FSharp.DependencyManager.Nuget.xml +++ b/lib/fcs/FSharp.DependencyManager.Nuget.xml @@ -97,13 +97,13 @@ %s for F# %s - (Originally from /home/dev/Projects/fsharp/src/Compiler/Facilities/UtilsStrings.txt:2) + (Originally from C:\Users\shaya\RiderProjects\fickle-fsharp\src\Compiler\Facilities\UtilsStrings.txt:2) Microsoft (R) F# Compiler version %s - (Originally from /home/dev/Projects/fsharp/src/Compiler/Facilities/UtilsStrings.txt:1) + (Originally from C:\Users\shaya\RiderProjects\fickle-fsharp\src\Compiler\Facilities\UtilsStrings.txt:1) diff --git a/lib/fcs/README.md b/lib/fcs/README.md index 553a0ac261..a4dda08061 100644 --- a/lib/fcs/README.md +++ b/lib/fcs/README.md @@ -1,9 +1,14 @@ # FSharp.Compiler.Service -This binary is generated by the following steps: +These binaries come from Fickle's FCS fork, , branch +`fickle_slim`. That branch follows `ncave/fsharp@service_slim` (Fable's FCS) and targets net10.0 only. -- Clone https://github.com/ncave/fsharp -- Check out `service_slim` branch -- Run `bash fcs/build.sh` -- Copy `artifacts/bin/FSharp.Compiler.Service/Release/netstandard2.0/FSharp.Compiler.Service.*` -- Copy `artifacts/bin/FSharp.Compiler.Service/Release/netstandard2.0/FSharp.Core.*` +Regenerate them with: + +```bash +dotnet run --project src/Fickle.Build -- fcs-fork +``` + +It clones the fork next to this repository if it is missing (`--repo` points elsewhere), builds +`src/Compiler` in Release, and copies `FSharp.Compiler.Service.*`, `FSharp.DependencyManager.Nuget.*` and +`FSharp.Core.*` from `artifacts/bin/FSharp.Compiler.Service/Release/net10.0/` into this folder. diff --git a/notes/benchmarks/attribute-constructors/AttributeTests.fsproj b/notes/benchmarks/attribute-constructors/AttributeTests.fsproj new file mode 100644 index 0000000000..860d862e7e --- /dev/null +++ b/notes/benchmarks/attribute-constructors/AttributeTests.fsproj @@ -0,0 +1,12 @@ + + + Exe + net10.0 + true + + + + + + + diff --git a/notes/benchmarks/attribute-constructors/Program.fs b/notes/benchmarks/attribute-constructors/Program.fs new file mode 100644 index 0000000000..9c80449daf --- /dev/null +++ b/notes/benchmarks/attribute-constructors/Program.fs @@ -0,0 +1,139 @@ +open System +open System.IO +open FSharp.Compiler.CodeAnalysis +open FSharp.Compiler.Diagnostics +open FSharp.Compiler.SourceCodeServices +open FSharp.Compiler.Symbols + +let declaration constructors application = + "module Test\nopen System\n[]\ntype AAttribute =\n inherit Attribute\n" + + constructors + + "\n member _.Name with get() = \"\" and set(value: string) = ()\n[]\ntype Marked = class end\n" + +let cases = + [ + "ascending", + false, + declaration " new () = { inherit Attribute() }\n new (x: int) = { inherit Attribute() }" "1" + "descending", + false, + declaration " new (x: int) = { inherit Attribute() }\n new () = { inherit Attribute() }" "" + "equal-arity", + false, + declaration " new (x: string) = { inherit Attribute() }\n new (x: int) = { inherit Attribute() }" "1" + "named-property", + false, + declaration + " new () = { inherit Attribute() }\n new (x: int) = { inherit Attribute() }" + "1, Name = \"named\"" + "private", + true, + declaration " private new (x: int) = { inherit Attribute() }\n new () = { inherit Attribute() }" "1" + "bad-argument", + true, + declaration " new (x: string) = { inherit Attribute() }\n new (x: int) = { inherit Attribute() }" "true" + "bad-arity", + true, + declaration " new (x: int, y: int) = { inherit Attribute() }\n new () = { inherit Attribute() }" "1" + "ambiguous", + true, + declaration " new (x: string) = { inherit Attribute() }\n new (x: Type) = { inherit Attribute() }" "null" + "three-constructors", + false, + declaration + " new (x: string) = { inherit Attribute() }\n new (x: int) = { inherit Attribute() }\n new () = { inherit Attribute() }" + "\"str\"" + "imported", + false, + "module Test\n[]\ntype Marked = class end\nlet x (v: Marked) = v\n" + ] + +[] +let main argv = + let refs = + (AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES") :?> string).Split(Path.PathSeparator) + |> Array.map (fun p -> "-r:" + p) + + let snapshot = ResizeArray() + + for name, expectError, source in cases do + let path = Path.Combine(__SOURCE_DIRECTORY__, name + ".fs") + + let options: FSharpProjectOptions = + { + ProjectFileName = path + "proj" + ProjectId = None + SourceFiles = [| path |] + OtherOptions = + Array.append + [| + "--target:library" + "--targetprofile:netcore" + "--noframework" + "--langversion:preview" + |] + refs + ReferencedProjects = [||] + IsIncompleteTypeCheckEnvironment = false + UseScriptResolutionRules = false + LoadTime = DateTime.MaxValue + UnresolvedReferences = None + OriginalLoadReferences = [] + Stamp = None + } + + let checker = InteractiveChecker.Create(options) + + let result = + checker.ParseAndCheckProject(options.ProjectFileName, [| path |], fun _ -> 0, lazy source) + |> Async.RunSynchronously + + let hasError = + result.Diagnostics + |> Array.exists (fun d -> d.Severity = FSharpDiagnosticSeverity.Error) + + if hasError <> expectError then + failwithf "%s: unexpected diagnostics %A" name result.Diagnostics + + snapshot.Add(name) + + for d in result.Diagnostics do + snapshot.Add( + sprintf + "%d|%A|%d:%d-%d:%d|%s" + d.ErrorNumber + d.Severity + d.StartLine + d.StartColumn + d.EndLine + d.EndColumn + d.Message + ) + + let rec inspect (entity: FSharpEntity) = + if entity.DisplayName = "Marked" then + for attr in entity.Attributes do + snapshot.Add( + sprintf + "attribute:%s|%A|%A" + attr.AttributeType.FullName + (Seq.toList attr.ConstructorArguments) + (Seq.toList attr.NamedArguments) + ) + + for child in entity.NestedEntities do + inspect child + + for entity in result.AssemblySignature.Entities do + inspect entity + + printfn "%s: %d diagnostics" name result.Diagnostics.Length + + File.WriteAllLines(argv[0], snapshot) + + if argv.Length > 1 && File.ReadAllText(argv[0]) <> File.ReadAllText(argv[1]) then + failwith "Attribute diagnostics changed (including ordering, messages or ranges)" + + 0 diff --git a/notes/benchmarks/attribute-constructors/baseline.txt b/notes/benchmarks/attribute-constructors/baseline.txt new file mode 100644 index 0000000000..024a45f8b9 --- /dev/null +++ b/notes/benchmarks/attribute-constructors/baseline.txt @@ -0,0 +1,50 @@ +ascending +attribute:Test.AAttribute|[(type Microsoft.FSharp.Core.int, 1)]|[] +descending +attribute:Test.AAttribute|[]|[] +equal-arity +attribute:Test.AAttribute|[(type Microsoft.FSharp.Core.int, 1)]|[] +named-property +attribute:Test.AAttribute|[(type Microsoft.FSharp.Core.int, 1)]|[(type Microsoft.FSharp.Core.string, "Name", true, "named")] +private +501|Error|9:2-9:6|The object constructor 'AAttribute' takes 0 argument(s) but is here given 1. The required signature is 'new: unit -> AAttribute'. +501|Error|9:2-9:6|The object constructor 'AAttribute' takes 0 argument(s) but is here given 1. The required signature is 'new: unit -> AAttribute'. +bad-argument +41|Error|9:2-9:9|No overloads match for method 'AAttribute'. + +Known type of argument: bool + +Available overloads: + - new: x: int -> AAttribute // Argument 'x' doesn't match + - new: x: string -> AAttribute // Argument 'x' doesn't match +41|Error|9:2-9:9|No overloads match for method 'AAttribute'. + +Known type of argument: bool + +Available overloads: + - new: x: int -> AAttribute // Argument 'x' doesn't match + - new: x: string -> AAttribute // Argument 'x' doesn't match +bad-arity +505|Error|9:2-9:6|The member or object constructor 'AAttribute' does not take 1 argument(s). An overload was found taking 0 arguments. +505|Error|9:2-9:6|The member or object constructor 'AAttribute' does not take 1 argument(s). An overload was found taking 0 arguments. +ambiguous +41|Error|9:2-9:9|A unique overload for method 'AAttribute' could not be determined based on type information prior to this program point. A type annotation may be needed. + +Known type of argument: 'a0 when 'a0: null + +Candidates: + - new: x: Type -> AAttribute + - new: x: string -> AAttribute +41|Error|9:2-9:9|A unique overload for method 'AAttribute' could not be determined based on type information prior to this program point. A type annotation may be needed. + +Known type of argument: 'a0 when 'a0: null + +Candidates: + - new: x: Type -> AAttribute + - new: x: string -> AAttribute +three-constructors +attribute:Test.AAttribute|[(type Microsoft.FSharp.Core.string, "str")]|[] +imported +44|Warning|4:10-4:16|This construct is deprecated. message +attribute:System.ObsoleteAttribute|[(type Microsoft.FSharp.Core.string, "message"); + (type Microsoft.FSharp.Core.bool, false)]|[] diff --git a/notes/benchmarks/attribute-constructors/comparison.csv b/notes/benchmarks/attribute-constructors/comparison.csv new file mode 100644 index 0000000000..eb2d28945a --- /dev/null +++ b/notes/benchmarks/attribute-constructors/comparison.csv @@ -0,0 +1,25 @@ +"Project","Variant","Round","WallMs","CheckMs","CompileMs","AllocatedMB","IdenticalFiles" +"binding","attribute-base","0","3581","1719","1874","1219","68" +"binding","attribute-candidate","0","3735","1800","1960","1219","68" +"binding","attribute-candidate","1","4825","2001","2158","1219","68" +"binding","attribute-base","1","4542","2155","2347","1219","68" +"binding","attribute-candidate","2","4434","2138","2273","1218","68" +"binding","attribute-base","2","4202","1819","1943","1220","68" +"binding","attribute-base","3","5192","2816","3327","1220","68" +"binding","attribute-candidate","3","6931","2159","2314","1218","68" +"binding","attribute-base","4","4354","2063","2220","1219","68" +"binding","attribute-candidate","4","3820","1811","1953","1219","68" +"binding","attribute-candidate","5","3762","1791","1938","1219","68" +"binding","attribute-base","5","3852","1834","1967","1219","68" +"js","attribute-candidate","0","6549","2106","2471","7809","141" +"js","attribute-base","0","5507","2366","2714","7807","141" +"js","attribute-base","1","6611","2614","3002","7799","141" +"js","attribute-candidate","1","5292","2201","2514","7810","141" +"js","attribute-base","2","4818","2184","2535","7804","141" +"js","attribute-candidate","2","5347","2228","2545","7805","141" +"js","attribute-candidate","3","4532","1930","2243","7812","141" +"js","attribute-base","3","4725","2082","2398","7806","141" +"js","attribute-candidate","4","4973","2015","2299","7809","141" +"js","attribute-base","4","4604","2127","2432","7810","141" +"js","attribute-base","5","4674","2129","2443","7806","141" +"js","attribute-candidate","5","4831","2210","2530","7806","141" diff --git a/notes/benchmarks/benchmark-0.5.3.py b/notes/benchmarks/benchmark-0.5.3.py new file mode 100644 index 0000000000..9fcd169cc3 --- /dev/null +++ b/notes/benchmarks/benchmark-0.5.3.py @@ -0,0 +1,146 @@ +import csv +import json +import os +from pathlib import Path +import re +import shutil +import subprocess +import time + +ROOT = Path(__file__).resolve().parents[2] +OUT = ROOT / "temp/release-0.5.3/benchmark" +OUT.mkdir(parents=True, exist_ok=True) +import argparse + +parser = argparse.ArgumentParser(description="Repeat the Fickl 0.5.3 six-workload comparison") +parser.add_argument( + "--scratch", type=Path, required=True, help="Previous harness scratchpad containing bench/ and upfable/" +) +parser.add_argument("--tool", type=Path, required=True, help="Packaged Fickl fable.exe") +parser.add_argument("--resume", action="store_true", help="Keep completed workloads and rerun incomplete workloads") +args = parser.parse_args() +SCRATCH = args.scratch.resolve() +TOOL = args.tool.resolve() +UPSTREAM = SCRATCH / "upfable/fable.exe" +PROJECTS = [ + ("py", SCRATCH / "bench/pybench", "App.fsproj", "python", []), + ("beam", SCRATCH / "bench/beambench", "App.fsproj", "beam", []), + ("sutil", SCRATCH / "bench/Sutil/src/App", "App.fsproj", "javascript", []), + ("fantomas", SCRATCH / "bench/fantomas-tools/src/client/fsharp", "FantomasTools.fsproj", "javascript", []), + ("ionide", SCRATCH / "bench/ionide-vscode-fsharp/src", "Ionide.FSharp.fsproj", "javascript", []), + ("jssuite", ROOT / "tests/Js/Main", "Fable.Tests.fsproj", "javascript", ["--exclude", "Fable.Core"]), +] + + +def run(project, cfg, round_no): + name, directory, project_file, language, extra = project + output = OUT / "output" / f"{name}-{cfg}" + assert output.resolve().is_relative_to(OUT.resolve()) + if output.exists(): + shutil.rmtree(output) + env = os.environ.copy() + env.pop("DOTNET_CLI_TELEMETRY_OPTOUT", None) + env.pop("FICKL_NUGET_KEY", None) + if cfg == "fableq": + env["DOTNET_CLI_TELEMETRY_OPTOUT"] = "1" + env["FICKL_PACKAGE_CACHE"] = str(OUT / "package-cache") + command = [ + "rtk", + "proxy", + str(UPSTREAM if cfg.startswith("fable") else TOOL), + project_file, + "--lang", + language, + *extra, + "--outDir", + str(output), + "--noCache", + ] + if cfg == "nopkg": + command += ["--noPackageCache"] + if cfg == "miss": + env["FICKL_PACKAGE_CACHE"] = str(OUT / f"miss-cache-{name}") + started = time.perf_counter() + result = subprocess.run( + command, cwd=directory, env=env, capture_output=True, text=True, encoding="utf-8", errors="replace" + ) + wall = round((time.perf_counter() - started) * 1000) + log = result.stdout + result.stderr + (OUT / f"{name}-{cfg}-{round_no}.log").write_text(log, encoding="utf-8") + + def timing(pattern): + matches = re.findall(pattern, log) + return int(matches[-1]) if matches else None + + row = dict( + project=name, + configuration=cfg, + run=round_no, + wall_ms=wall, + parsed_ms=timing(r"parsed in (\d+)ms"), + compile_ms=timing(r"compilation finished in (\d+)ms"), + exit_code=result.returncode, + ) + with (OUT / "results.csv").open("a", newline="", encoding="utf-8") as file: + writer = csv.DictWriter(file, fieldnames=list(row)) + if file.tell() == 0: + writer.writeheader() + writer.writerow(row) + print(json.dumps(row), flush=True) + if result.returncode or row["compile_ms"] is None: + raise RuntimeError(f"Benchmark failed: {name}/{cfg}/{round_no}; see saved log") + + +if (OUT / "results.csv").exists() and not args.resume: + raise FileExistsError(f"Archive the previous results before rerunning: {OUT}") + +if args.resume and (OUT / "results.csv").exists(): + previous = list(csv.DictReader((OUT / "results.csv").open(newline=""))) + complete = { + p[0] + for p in PROJECTS + if all( + len( + [ + r + for r in previous + if r["project"] == p[0] + and r["configuration"] == cfg + and int(r["run"]) > 0 + and r["exit_code"] == "0" + ] + ) + == 5 + for cfg in ["fable", "fableq", "nopkg", "hit"] + ) + } + shutil.copy2(OUT / "results.csv", OUT / f"results-before-resume-{time.time_ns()}.csv") + with (OUT / "results.csv").open("w", newline="") as file: + writer = csv.DictWriter(file, fieldnames=previous[0].keys()) + writer.writeheader() + writer.writerows(r for r in previous if r["project"] in complete) + PROJECTS = [p for p in PROJECTS if p[0] not in complete] + +try: + for project in PROJECTS: + for config in ["fable", "fableq", "nopkg", "hit"]: + run(project, config, -1) + run(project, "miss", 0) + for round_no in range(1, 6): + order = ["fable", "hit", "fableq", "nopkg"] if round_no % 2 else ["nopkg", "fableq", "hit", "fable"] + for config in order: + run(project, config, round_no) +finally: + subprocess.run( + [ + "rtk", + "proxy", + "powershell", + "-NoProfile", + "-ExecutionPolicy", + "Bypass", + "-File", + str(Path(__file__).with_name("cleanup-msbuild.ps1")), + ], + check=True, + ) diff --git a/notes/benchmarks/cleanup-msbuild.ps1 b/notes/benchmarks/cleanup-msbuild.ps1 new file mode 100644 index 0000000000..f88d7a7070 --- /dev/null +++ b/notes/benchmarks/cleanup-msbuild.ps1 @@ -0,0 +1,22 @@ +$ErrorActionPreference = 'Stop' +function Get-LeftoverMSBuild { + $processes = @(Get-CimInstance Win32_Process) + $rider = @($processes | Where-Object { $_.Name -ieq 'RiderStub.exe' } | Select-Object -ExpandProperty ProcessId) + $processes | Where-Object { + # Rider launches active project evaluations repeatedly; these are not leftover benchmark workers. + $_.ParentProcessId -notin $rider -and ( + $_.Name -ieq 'MSBuild.exe' -or + ($_.Name -ieq 'dotnet.exe' -and $_.CommandLine -match '(?i)(MSBuild\.dll|\s+msbuild\s)') + ) + } +} +$leftovers = @(Get-LeftoverMSBuild) +foreach ($worker in $leftovers) { + Write-Output "Stopping leftover MSBuild process $($worker.ProcessId)" + Stop-Process -Id $worker.ProcessId -Force -ErrorAction SilentlyContinue +} +if ($leftovers.Count -gt 0) { Wait-Process -Id $leftovers.ProcessId -Timeout 5 -ErrorAction SilentlyContinue } +Write-Output "MSBuild cleanup complete: $($leftovers.Count) processes stopped." +$remaining = @(Get-LeftoverMSBuild) +if ($remaining.Count -gt 0) { throw "$($remaining.Count) MSBuild processes remain after cleanup" } +Write-Output 'Verified: no leftover MSBuild workers remain (active Rider evaluations excluded).' diff --git a/notes/benchmarks/cold-start/App.fs b/notes/benchmarks/cold-start/App.fs new file mode 100644 index 0000000000..39d79e8d07 --- /dev/null +++ b/notes/benchmarks/cold-start/App.fs @@ -0,0 +1,3 @@ +module App + +let add a b = a + b diff --git a/notes/benchmarks/cold-start/App.fsproj b/notes/benchmarks/cold-start/App.fsproj new file mode 100644 index 0000000000..e5925617fd --- /dev/null +++ b/notes/benchmarks/cold-start/App.fsproj @@ -0,0 +1,4 @@ + + net10.0 + + diff --git a/notes/benchmarks/cold-start/measure-pgo.py b/notes/benchmarks/cold-start/measure-pgo.py new file mode 100644 index 0000000000..c7f40177db --- /dev/null +++ b/notes/benchmarks/cold-start/measure-pgo.py @@ -0,0 +1,75 @@ +import csv +import hashlib +import os +from pathlib import Path +import re +import subprocess +import time + +root = Path(__file__).resolve().parents[3] +dest = root / "temp/cold-start" +tool = root / "temp/release-0.5.3/verified-package/tools/net10.0/win-x64/fable.exe" +env = dict(os.environ, DOTNET_CLI_TELEMETRY_OPTOUT="1") +rows = [] +expected = {} +for workload, project, extra in [ + ("tiny", Path(__file__).with_name("App.fsproj"), []), + ("js", root / "tests/Js/Main/Fable.Tests.fsproj", ["--exclude", "Fable.Core"]), +]: + for run in range(6): + configs = ["default", "pgo-off"] if run % 2 == 0 else ["pgo-off", "default"] + if workload == "version": + configs = ["version"] + for cfg in configs: + output = dest / f"output-{workload}" + args = ( + ["--version"] + if project is None + else [str(project), "--outDir", str(output), "--noCache", "--noPackageCache", "--timings", *extra] + ) + if cfg == "no-restore": + args.append("--noRestore") + if cfg == "pgo-off": + env["DOTNET_TieredPGO"] = "0" + else: + env.pop("DOTNET_TieredPGO", None) + start = time.perf_counter() + result = subprocess.run( + ["rtk", "proxy", str(tool), *args], + env=env, + capture_output=True, + text=True, + encoding="utf-8", + errors="replace", + ) + wall = round((time.perf_counter() - start) * 1000) + log = result.stdout + result.stderr + (dest / f"{workload}-{cfg}-{run}.log").write_text(log, encoding="utf-8") + if result.returncode: + raise RuntimeError(log) + + def timing(pattern): + found = re.search(pattern, log) + return int(found[1]) if found else 0 + + if project: + hashes = { + str(p.relative_to(output)): hashlib.sha256(p.read_bytes()).hexdigest() for p in output.rglob("*.js") + } + if workload not in expected: + expected[workload] = hashes + assert hashes == expected[workload], "Generated output differs" + row = dict( + workload=workload, + config=cfg, + run=run, + wall_ms=wall, + crack_ms=timing(r"Project cracking \(wall\)\s+(\d+) ms"), + compile_ms=timing(r"Compilation total \(wall\)\s+(\d+) ms"), + ) + rows.append(row) + print(row, flush=True) + with (dest / "pgo-results.csv").open("w", newline="") as f: + writer = csv.DictWriter(f, fieldnames=row.keys()) + writer.writeheader() + writer.writerows(rows) diff --git a/notes/benchmarks/cold-start/measure.py b/notes/benchmarks/cold-start/measure.py new file mode 100644 index 0000000000..eabc282d5e --- /dev/null +++ b/notes/benchmarks/cold-start/measure.py @@ -0,0 +1,72 @@ +import csv +import hashlib +import os +from pathlib import Path +import re +import subprocess +import time + +root = Path(__file__).resolve().parents[3] +dest = root / "temp/cold-start" +tool = root / "temp/release-0.5.3/verified-package/tools/net10.0/win-x64/fable.exe" +env = dict(os.environ, DOTNET_CLI_TELEMETRY_OPTOUT="1") +rows = [] +expected = {} +for workload, project, extra in [ + ("version", None, []), + ("tiny", Path(__file__).with_name("App.fsproj"), []), + ("js", root / "tests/Js/Main/Fable.Tests.fsproj", ["--exclude", "Fable.Core"]), +]: + for run in range(6): + configs = ["restore", "no-restore"] if run % 2 == 0 else ["no-restore", "restore"] + if workload == "version": + configs = ["version"] + for cfg in configs: + output = dest / f"output-{workload}" + args = ( + ["--version"] + if project is None + else [str(project), "--outDir", str(output), "--noCache", "--noPackageCache", "--timings", *extra] + ) + if cfg == "no-restore": + args.append("--noRestore") + start = time.perf_counter() + result = subprocess.run( + ["rtk", "proxy", str(tool), *args], + env=env, + capture_output=True, + text=True, + encoding="utf-8", + errors="replace", + ) + wall = round((time.perf_counter() - start) * 1000) + log = result.stdout + result.stderr + (dest / f"{workload}-{cfg}-{run}.log").write_text(log, encoding="utf-8") + if result.returncode: + raise RuntimeError(log) + + def timing(pattern): + found = re.search(pattern, log) + return int(found[1]) if found else 0 + + if project: + hashes = { + str(p.relative_to(output)): hashlib.sha256(p.read_bytes()).hexdigest() for p in output.rglob("*.js") + } + if workload not in expected: + expected[workload] = hashes + assert hashes == expected[workload], "Generated output differs" + row = dict( + workload=workload, + config=cfg, + run=run, + wall_ms=wall, + crack_ms=timing(r"Project cracking \(wall\)\s+(\d+) ms"), + compile_ms=timing(r"Compilation total \(wall\)\s+(\d+) ms"), + ) + rows.append(row) + print(row, flush=True) + with (dest / "results.csv").open("w", newline="") as f: + writer = csv.DictWriter(f, fieldnames=row.keys()) + writer.writeheader() + writer.writerows(rows) diff --git a/notes/benchmarks/cold-start/pgo-results.csv b/notes/benchmarks/cold-start/pgo-results.csv new file mode 100644 index 0000000000..9d5a0250de --- /dev/null +++ b/notes/benchmarks/cold-start/pgo-results.csv @@ -0,0 +1,25 @@ +workload,config,run,wall_ms,crack_ms,compile_ms +tiny,default,0,1392,754,243 +tiny,pgo-off,0,1442,785,240 +tiny,pgo-off,1,1440,807,241 +tiny,default,1,1373,746,225 +tiny,default,2,1428,798,236 +tiny,pgo-off,2,1406,759,239 +tiny,pgo-off,3,1448,822,238 +tiny,default,3,1452,806,236 +tiny,default,4,1421,799,235 +tiny,pgo-off,4,1369,751,231 +tiny,pgo-off,5,1496,829,270 +tiny,default,5,1468,836,244 +js,default,0,4245,1562,2148 +js,pgo-off,0,4673,1591,2483 +js,pgo-off,1,4411,1660,2175 +js,default,1,4295,1527,2169 +js,default,2,4154,1477,2096 +js,pgo-off,2,4467,1579,2323 +js,pgo-off,3,4486,1582,2346 +js,default,3,4399,1619,2203 +js,default,4,4349,1533,2258 +js,pgo-off,4,4521,1460,2483 +js,pgo-off,5,4375,1490,2284 +js,default,5,4228,1463,2224 diff --git a/notes/benchmarks/cold-start/results.csv b/notes/benchmarks/cold-start/results.csv new file mode 100644 index 0000000000..679347a998 --- /dev/null +++ b/notes/benchmarks/cold-start/results.csv @@ -0,0 +1,31 @@ +workload,config,run,wall_ms,crack_ms,compile_ms +version,version,0,143,0,0 +version,version,1,135,0,0 +version,version,2,132,0,0 +version,version,3,130,0,0 +version,version,4,123,0,0 +version,version,5,125,0,0 +tiny,restore,0,1506,898,226 +tiny,no-restore,0,1265,558,281 +tiny,no-restore,1,1286,626,254 +tiny,restore,1,1459,790,252 +tiny,restore,2,1477,851,236 +tiny,no-restore,2,1195,546,241 +tiny,no-restore,3,1175,562,222 +tiny,restore,3,1452,799,250 +tiny,restore,4,1455,804,242 +tiny,no-restore,4,1212,595,230 +tiny,no-restore,5,1187,561,234 +tiny,restore,5,1389,793,226 +js,restore,0,5030,2102,2342 +js,no-restore,0,4187,1284,2314 +js,no-restore,1,4187,1252,2329 +js,restore,1,4617,1850,2203 +js,restore,2,4560,1715,2318 +js,no-restore,2,3994,1283,2140 +js,no-restore,3,4110,1200,2355 +js,restore,3,4270,1559,2172 +js,restore,4,4383,1537,2289 +js,no-restore,4,4244,1240,2471 +js,no-restore,5,4149,1308,2287 +js,restore,5,4301,1549,2172 diff --git a/notes/benchmarks/constructor-resolution/instrumentation.patch b/notes/benchmarks/constructor-resolution/instrumentation.patch new file mode 100644 index 0000000000..3559b659d9 --- /dev/null +++ b/notes/benchmarks/constructor-resolution/instrumentation.patch @@ -0,0 +1,31 @@ +--- a/src/Compiler/Checking/NameResolution.fs ++++ b/src/Compiler/Checking/NameResolution.fs +@@ -2536,12 +2536,14 @@ + //------------------------------------------------------------------------- + + let private ResolveObjectConstructorPrim (ncenv: NameResolver) edenv resInfo m ad ty = ++ let debugStart = System.Diagnostics.Stopwatch.GetTimestamp() + let g = ncenv.g + let amap = ncenv.amap + if isDelegateTy g ty then + success (resInfo, Item.DelegateCtor ty) + else + let ctorInfos = GetIntrinsicConstructorInfosOfType ncenv.InfoReader m ty ++ let debugLookup = System.Diagnostics.Stopwatch.GetTimestamp() + if isNil ctorInfos && isInterfaceTy g ty then + let tcref = tcrefOfAppTy g ty + success (resInfo, Item.Types(tcref.DisplayName, [ty])) +@@ -2559,7 +2561,12 @@ + else + let ctorInfos = ctorInfos |> List.filter (IsMethInfoAccessible amap m ad) + let metadataTy = convertToTypeWithMetadataIfPossible g ty +- success (resInfo, Item.MakeCtorGroup ((tcrefOfAppTy g metadataTy).LogicalName, (defaultStructCtorInfo@ctorInfos))) ++ let debugSortStart = System.Diagnostics.Stopwatch.GetTimestamp() ++ let tcref = tcrefOfAppTy g metadataTy ++ let item = Item.MakeCtorGroup (tcref.LogicalName, (defaultStructCtorInfo@ctorInfos)) ++ let debugEnd = System.Diagnostics.Stopwatch.GetTimestamp() ++ System.Console.Error.WriteLine(sprintf "[DEBUG-CTOR]|%s|%b|%s|%d|%d|%d|%d|%d" tcref.LogicalName tcref.IsLocalRef m.FileName m.StartLine ctorInfos.Length (debugLookup-debugStart) (debugSortStart-debugLookup) (debugEnd-debugSortStart)) ++ success (resInfo, item) + + /// Perform name resolution for an identifier which must resolve to be an object constructor. + let ResolveObjectConstructor (ncenv: NameResolver) denv m ad ty = diff --git a/notes/benchmarks/constructor-resolution/measurements.json b/notes/benchmarks/constructor-resolution/measurements.json new file mode 100644 index 0000000000..f962147b16 --- /dev/null +++ b/notes/benchmarks/constructor-resolution/measurements.json @@ -0,0 +1,475 @@ +[ + { + "variant": "../attribute-base", + "identical_files": 67, + "groups": [] + }, + { + "variant": "instrumented", + "identical_files": 67, + "groups": [ + { + "name": "OptionalArgumentAttribute", + "local": "false", + "constructors": 1, + "calls": 1041, + "unique_locations": 561, + "lookup_ms": 4.8265, + "filter_ms": 3.671, + "sort_ms": 0.1974 + }, + { + "name": "AllowNullLiteralAttribute", + "local": "false", + "constructors": 2, + "calls": 910, + "unique_locations": 910, + "lookup_ms": 1.2198, + "filter_ms": 2.349, + "sort_ms": 2.7149 + }, + { + "name": "InterfaceAttribute", + "local": "false", + "constructors": 1, + "calls": 910, + "unique_locations": 910, + "lookup_ms": 0.9563, + "filter_ms": 1.4054, + "sort_ms": 0.1155 + }, + { + "name": "EmitAttribute", + "local": "false", + "constructors": 2, + "calls": 749, + "unique_locations": 684, + "lookup_ms": 1.1659, + "filter_ms": 2.9208, + "sort_ms": 2.5649 + }, + { + "name": "RequireQualifiedAccessAttribute", + "local": "false", + "constructors": 1, + "calls": 262, + "unique_locations": 131, + "lookup_ms": 0.3483, + "filter_ms": 0.5895, + "sort_ms": 0.0298 + }, + { + "name": "CompiledNameAttribute", + "local": "false", + "constructors": 1, + "calls": 244, + "unique_locations": 244, + "lookup_ms": 0.3396, + "filter_ms": 0.8014, + "sort_ms": 0.0274 + }, + { + "name": "StringEnumAttribute", + "local": "false", + "constructors": 2, + "calls": 150, + "unique_locations": 75, + "lookup_ms": 0.1646, + "filter_ms": 0.3473, + "sort_ms": 0.4008 + }, + { + "name": "ObsoleteAttribute", + "local": "false", + "constructors": 3, + "calls": 130, + "unique_locations": 130, + "lookup_ms": 1.1133, + "filter_ms": 0.7629, + "sort_ms": 0.4842 + }, + { + "name": "ParamObjectAttribute", + "local": "false", + "constructors": 2, + "calls": 130, + "unique_locations": 65, + "lookup_ms": 0.2066, + "filter_ms": 0.3807, + "sort_ms": 0.3075 + }, + { + "name": "EraseAttribute", + "local": "false", + "constructors": 2, + "calls": 22, + "unique_locations": 11, + "lookup_ms": 0.3705, + "filter_ms": 0.0581, + "sort_ms": 0.8791 + }, + { + "name": "ParamArrayAttribute", + "local": "false", + "constructors": 1, + "calls": 16, + "unique_locations": 16, + "lookup_ms": 0.0727, + "filter_ms": 0.0196, + "sort_ms": 0.0013 + }, + { + "name": "AbstractClassAttribute", + "local": "false", + "constructors": 1, + "calls": 12, + "unique_locations": 6, + "lookup_ms": 24.0103, + "filter_ms": 21.4695, + "sort_ms": 1.025 + }, + { + "name": "EmitIndexerAttribute", + "local": "false", + "constructors": 1, + "calls": 10, + "unique_locations": 10, + "lookup_ms": 0.2749, + "filter_ms": 0.0362, + "sort_ms": 0.0016 + }, + { + "name": "CompiledValueAttribute", + "local": "false", + "constructors": 4, + "calls": 3, + "unique_locations": 3, + "lookup_ms": 0.0631, + "filter_ms": 0.0313, + "sort_ms": 0.36 + }, + { + "name": "ImportAllAttribute", + "local": "false", + "constructors": 1, + "calls": 2, + "unique_locations": 1, + "lookup_ms": 0.024, + "filter_ms": 0.02, + "sort_ms": 0.0005 + } + ] + }, + { + "variant": "instrumented", + "identical_files": 67, + "groups": [ + { + "name": "OptionalArgumentAttribute", + "local": "false", + "constructors": 1, + "calls": 1041, + "unique_locations": 561, + "lookup_ms": 0.9167, + "filter_ms": 1.3635, + "sort_ms": 0.086 + }, + { + "name": "AllowNullLiteralAttribute", + "local": "false", + "constructors": 2, + "calls": 910, + "unique_locations": 910, + "lookup_ms": 0.9529, + "filter_ms": 1.1683, + "sort_ms": 2.442 + }, + { + "name": "InterfaceAttribute", + "local": "false", + "constructors": 1, + "calls": 910, + "unique_locations": 910, + "lookup_ms": 0.8012, + "filter_ms": 1.2592, + "sort_ms": 0.0844 + }, + { + "name": "EmitAttribute", + "local": "false", + "constructors": 2, + "calls": 749, + "unique_locations": 684, + "lookup_ms": 0.4157, + "filter_ms": 0.9928, + "sort_ms": 0.8703 + }, + { + "name": "RequireQualifiedAccessAttribute", + "local": "false", + "constructors": 1, + "calls": 262, + "unique_locations": 131, + "lookup_ms": 0.2487, + "filter_ms": 0.3306, + "sort_ms": 0.024 + }, + { + "name": "CompiledNameAttribute", + "local": "false", + "constructors": 1, + "calls": 244, + "unique_locations": 244, + "lookup_ms": 0.1781, + "filter_ms": 0.279, + "sort_ms": 0.018 + }, + { + "name": "StringEnumAttribute", + "local": "false", + "constructors": 2, + "calls": 150, + "unique_locations": 75, + "lookup_ms": 0.1363, + "filter_ms": 0.3061, + "sort_ms": 0.3842 + }, + { + "name": "ObsoleteAttribute", + "local": "false", + "constructors": 3, + "calls": 130, + "unique_locations": 130, + "lookup_ms": 0.8816, + "filter_ms": 0.5091, + "sort_ms": 0.2527 + }, + { + "name": "ParamObjectAttribute", + "local": "false", + "constructors": 2, + "calls": 130, + "unique_locations": 65, + "lookup_ms": 0.1453, + "filter_ms": 0.276, + "sort_ms": 0.2596 + }, + { + "name": "EraseAttribute", + "local": "false", + "constructors": 2, + "calls": 22, + "unique_locations": 11, + "lookup_ms": 0.326, + "filter_ms": 0.0479, + "sort_ms": 1.5005 + }, + { + "name": "ParamArrayAttribute", + "local": "false", + "constructors": 1, + "calls": 16, + "unique_locations": 16, + "lookup_ms": 0.0358, + "filter_ms": 0.0135, + "sort_ms": 0.0012 + }, + { + "name": "AbstractClassAttribute", + "local": "false", + "constructors": 1, + "calls": 12, + "unique_locations": 6, + "lookup_ms": 5.3888, + "filter_ms": 6.6441, + "sort_ms": 0.2072 + }, + { + "name": "EmitIndexerAttribute", + "local": "false", + "constructors": 1, + "calls": 10, + "unique_locations": 10, + "lookup_ms": 0.0303, + "filter_ms": 0.0362, + "sort_ms": 0.0015 + }, + { + "name": "CompiledValueAttribute", + "local": "false", + "constructors": 4, + "calls": 3, + "unique_locations": 3, + "lookup_ms": 0.0328, + "filter_ms": 0.0145, + "sort_ms": 0.2441 + }, + { + "name": "ImportAllAttribute", + "local": "false", + "constructors": 1, + "calls": 2, + "unique_locations": 1, + "lookup_ms": 0.0234, + "filter_ms": 0.0213, + "sort_ms": 0.0004 + } + ] + }, + { + "variant": "instrumented", + "identical_files": 67, + "groups": [ + { + "name": "OptionalArgumentAttribute", + "local": "false", + "constructors": 1, + "calls": 1041, + "unique_locations": 561, + "lookup_ms": 1.0087, + "filter_ms": 1.8469, + "sort_ms": 0.1191 + }, + { + "name": "AllowNullLiteralAttribute", + "local": "false", + "constructors": 2, + "calls": 910, + "unique_locations": 910, + "lookup_ms": 0.6145, + "filter_ms": 0.7647, + "sort_ms": 11.0046 + }, + { + "name": "InterfaceAttribute", + "local": "false", + "constructors": 1, + "calls": 910, + "unique_locations": 910, + "lookup_ms": 0.5218, + "filter_ms": 0.6235, + "sort_ms": 0.0608 + }, + { + "name": "EmitAttribute", + "local": "false", + "constructors": 2, + "calls": 749, + "unique_locations": 684, + "lookup_ms": 0.9551, + "filter_ms": 2.4774, + "sort_ms": 1.6103 + }, + { + "name": "RequireQualifiedAccessAttribute", + "local": "false", + "constructors": 1, + "calls": 262, + "unique_locations": 131, + "lookup_ms": 0.1825, + "filter_ms": 0.2196, + "sort_ms": 0.0214 + }, + { + "name": "CompiledNameAttribute", + "local": "false", + "constructors": 1, + "calls": 244, + "unique_locations": 244, + "lookup_ms": 0.2627, + "filter_ms": 0.3136, + "sort_ms": 0.0236 + }, + { + "name": "StringEnumAttribute", + "local": "false", + "constructors": 2, + "calls": 150, + "unique_locations": 75, + "lookup_ms": 0.1148, + "filter_ms": 0.1846, + "sort_ms": 0.2929 + }, + { + "name": "ObsoleteAttribute", + "local": "false", + "constructors": 3, + "calls": 130, + "unique_locations": 130, + "lookup_ms": 0.6473, + "filter_ms": 0.5511, + "sort_ms": 0.336 + }, + { + "name": "ParamObjectAttribute", + "local": "false", + "constructors": 2, + "calls": 130, + "unique_locations": 65, + "lookup_ms": 0.1752, + "filter_ms": 0.3456, + "sort_ms": 1.1762 + }, + { + "name": "EraseAttribute", + "local": "false", + "constructors": 2, + "calls": 22, + "unique_locations": 11, + "lookup_ms": 0.3343, + "filter_ms": 0.0425, + "sort_ms": 0.7458 + }, + { + "name": "ParamArrayAttribute", + "local": "false", + "constructors": 1, + "calls": 16, + "unique_locations": 16, + "lookup_ms": 0.0406, + "filter_ms": 0.0116, + "sort_ms": 0.0013 + }, + { + "name": "AbstractClassAttribute", + "local": "false", + "constructors": 1, + "calls": 12, + "unique_locations": 6, + "lookup_ms": 6.5112, + "filter_ms": 6.526, + "sort_ms": 0.2912 + }, + { + "name": "EmitIndexerAttribute", + "local": "false", + "constructors": 1, + "calls": 10, + "unique_locations": 10, + "lookup_ms": 0.0261, + "filter_ms": 0.0225, + "sort_ms": 0.0011 + }, + { + "name": "CompiledValueAttribute", + "local": "false", + "constructors": 4, + "calls": 3, + "unique_locations": 3, + "lookup_ms": 0.0308, + "filter_ms": 0.0105, + "sort_ms": 0.1824 + }, + { + "name": "ImportAllAttribute", + "local": "false", + "constructors": 1, + "calls": 2, + "unique_locations": 1, + "lookup_ms": 0.0277, + "filter_ms": 0.0237, + "sort_ms": 0.0004 + } + ] + } +] \ No newline at end of file diff --git a/notes/benchmarks/core-runtime/Program.cs b/notes/benchmarks/core-runtime/Program.cs new file mode 100644 index 0000000000..361e83e63d --- /dev/null +++ b/notes/benchmarks/core-runtime/Program.cs @@ -0,0 +1,22 @@ +using System; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Collections.Generic; +var assembly = Assembly.LoadFrom(Path.GetFullPath(args[0])); +var lines = new List(); +foreach (var type in assembly.GetExportedTypes()) { + lines.Add($"TYPE {type.FullName} {type.Attributes} {type.BaseType}"); + foreach (var face in type.GetInterfaces()) lines.Add($"INTERFACE {type.FullName} {face}"); + foreach (var member in type.GetMembers(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly)) + lines.Add($"MEMBER {type.FullName} {member.MemberType} {member}"); +} +lines.Sort(StringComparer.Ordinal); +File.WriteAllLines(args[1], lines); +Console.WriteLine($"{assembly.FullName}: {lines.Count} API entries"); +var anon = assembly.GetType("Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`1").MakeGenericType(typeof(int)); +var first = Activator.CreateInstance(anon, new object[] { 42 }); +var second = Activator.CreateInstance(anon, new object[] { 42 }); +Console.WriteLine($"AnonymousObject(42) equals separate instance: {first.Equals(second)}; sealed: {anon.IsSealed}"); +if (args.Length > 2 && first.Equals(second) != bool.Parse(args[2])) + throw new InvalidOperationException("AnonymousObject equality differs from the expected runtime behavior."); diff --git a/notes/benchmarks/core-runtime/Surface.csproj b/notes/benchmarks/core-runtime/Surface.csproj new file mode 100644 index 0000000000..169a8e6e1e --- /dev/null +++ b/notes/benchmarks/core-runtime/Surface.csproj @@ -0,0 +1,4 @@ + + Exenet10.0 + + diff --git a/notes/benchmarks/core-runtime/aligned-baseline-manifest.json b/notes/benchmarks/core-runtime/aligned-baseline-manifest.json new file mode 100644 index 0000000000..15b74780c7 --- /dev/null +++ b/notes/benchmarks/core-runtime/aligned-baseline-manifest.json @@ -0,0 +1,10 @@ +{ + "source_commit": "7d0437e8bcf9ead9483483eaa556f1998af8806b", + "dotnet_source_commit": "c23858a6d860abe3ca94f9ac630c3e61b2625664", + "runtime_package": "FSharp.Core/10.1.203", + "target_framework": "netstandard2.1", + "public_signed": true, + "runtime_sha256": "c71c2b7fde9cd3af4be358929ec65d4633f41aabf019e27b012c5f4701594dc8", + "baseline_sha256": "0d62ab9aed739975537e55caa4536a9e8fc7a2fcf11d9e6cb5135e23cfda5b9e", + "api_and_anonymous_object_equality_match": true +} \ No newline at end of file diff --git a/notes/benchmarks/core-runtime/aligned-map-comparison.csv b/notes/benchmarks/core-runtime/aligned-map-comparison.csv new file mode 100644 index 0000000000..e32e9eac35 --- /dev/null +++ b/notes/benchmarks/core-runtime/aligned-map-comparison.csv @@ -0,0 +1,25 @@ +"Project","Variant","Round","WallMs","CheckMs","CompileMs","AllocatedMB","IdenticalFiles" +"binding","baseline","0","3613","1643","1768","1219","67" +"binding","candidate","0","3437","1652","1788","1219","67" +"binding","candidate","1","3486","1652","1783","1219","67" +"binding","baseline","1","3428","1650","1783","1219","67" +"binding","baseline","2","3494","1669","1794","1220","67" +"binding","candidate","2","3474","1684","1815","1220","67" +"binding","candidate","3","3475","1646","1780","1219","67" +"binding","baseline","3","3439","1652","1776","1220","67" +"binding","baseline","4","3587","1758","1894","1220","67" +"binding","candidate","4","3697","1702","1842","1219","67" +"binding","candidate","5","3518","1684","1811","1219","67" +"binding","baseline","5","3549","1732","1872","1220","67" +"js","baseline","0","4540","1902","2209","7803","141" +"js","candidate","0","4385","2038","2309","7808","141" +"js","candidate","1","4275","1933","2228","7809","141" +"js","baseline","1","4170","1839","2158","7814","141" +"js","baseline","2","4247","1893","2202","7814","141" +"js","candidate","2","4214","1857","2152","7817","141" +"js","candidate","3","4554","2167","2474","7809","141" +"js","baseline","3","4388","2027","2302","7807","141" +"js","baseline","4","4185","1914","2176","7807","141" +"js","candidate","4","4346","2053","2335","7812","141" +"js","candidate","5","4342","1968","2273","7815","141" +"js","baseline","5","4175","1908","2164","7813","141" diff --git a/notes/benchmarks/core-runtime/allocations/Allocations.csproj b/notes/benchmarks/core-runtime/allocations/Allocations.csproj new file mode 100644 index 0000000000..4d44cc92e5 --- /dev/null +++ b/notes/benchmarks/core-runtime/allocations/Allocations.csproj @@ -0,0 +1,4 @@ + + Exenet10.0enable + + diff --git a/notes/benchmarks/core-runtime/allocations/Program.cs b/notes/benchmarks/core-runtime/allocations/Program.cs new file mode 100644 index 0000000000..36bd4a2fc0 --- /dev/null +++ b/notes/benchmarks/core-runtime/allocations/Program.cs @@ -0,0 +1,41 @@ +using Microsoft.Diagnostics.Tracing.Etlx; +using Microsoft.Diagnostics.Tracing.Parsers.Clr; +using System.Text.Json; + +var etlx = TraceLog.CreateFromEventPipeDataFile(args[0]); +using var log = new TraceLog(etlx); +var types = new Dictionary(); +var core = new Dictionary(); +var sites = new Dictionary(); +var selected = new Dictionary(); +double total = 0, checking = 0; +int ticks = 0, missingStacks = 0; +foreach (var ev in log.Events) { + if (ev is not GCAllocationTickTraceData allocation) continue; + ticks++; + double bytes = allocation.AllocationAmount64; + total += bytes; + var stack = ev.CallStack(); + if (stack == null) missingStacks++; + var frames = new List(); + for (; stack != null; stack = stack.Caller) + frames.Add(stack.CodeAddress.FullMethodName); + if (!frames.Any(f => f.Contains("FSharp.Compiler.CheckExpressions.") || f.Contains("FSharp.Compiler.CheckDeclarations.") || f.Contains("FSharp.Compiler.PostInferenceChecks."))) continue; + checking += bytes; + if (allocation.TypeName == "TypeToken") { + if (frames.Any(f => f.Contains("GenericHashArbArray"))) Add(selected, "TypeToken boxing under array hashing", bytes); + if (frames.Any(f => f.Contains("GenericEqualityArbArray"))) Add(selected, "TypeToken boxing under array equality", bytes); + } + if (allocation.TypeName.StartsWith("Microsoft.FSharp.Collections.MapTreeNode") && frames.Any(f => f.Contains("get_AllEntitiesByCompiledAndLogicalMangledNames"))) + Add(selected, "Map nodes under entity-name table construction", bytes); + Add(types, allocation.TypeName, bytes); + Add(sites, allocation.TypeName + " <- " + string.Join(" <- ", frames.Take(4)), bytes); + foreach (var frame in frames.Where(f => f.StartsWith("Microsoft.FSharp.")).Distinct()) + Add(core, frame, bytes); +} +var output = new { trace=args[0], ticks, missingStacks, eventsLost=log.EventsLost, totalSampledBytes=total, + checkingSampledBytes=checking, selected=Top(selected), types=Top(types), coreInclusive=Top(core), sites=Top(sites) }; +File.WriteAllText(args[1], JsonSerializer.Serialize(output, new JsonSerializerOptions { WriteIndented=true })); +Console.WriteLine($"{ticks} ticks, {missingStacks} without stacks; {checking/1048576:F1} estimated MB in checking stacks / {total/1048576:F1} total"); +static void Add(Dictionary d, string k, double v) => d[k] = d.GetValueOrDefault(k) + v; +static object Top(Dictionary d) => d.OrderByDescending(p => p.Value).Take(30).Select(p => new { name=p.Key, estimatedMB=p.Value/1048576 }).ToArray(); diff --git a/notes/benchmarks/core-runtime/analyze-stacks.py b/notes/benchmarks/core-runtime/analyze-stacks.py new file mode 100644 index 0000000000..ee96ce6549 --- /dev/null +++ b/notes/benchmarks/core-runtime/analyze-stacks.py @@ -0,0 +1,63 @@ +"""Rank sampled Core stacks during FCS checking; inclusive costs overlap.""" + +import collections +import json +import sys + +results = [] +for path in sys.argv[1:]: + data = json.load(open(path, encoding="utf-8")) + names = [f["name"].split("(")[0] for f in data["shared"]["frames"]] + inclusive, leaf, callers = (collections.Counter() for _ in range(3)) + total = core = 0 + for profile in data["profiles"]: + stack = [] + previous = profile["startValue"] + for event in profile["events"]: + elapsed = event["at"] - previous + frames = [names[i] for i in stack] if elapsed > 0.0001 and stack and names[stack[-1]] == "CPU_TIME" else [] + if ( + frames + and frames[-1] == "CPU_TIME" + and any( + any( + scope in f + for scope in ( + "FSharp.Compiler.CheckExpressions.", + "FSharp.Compiler.CheckDeclarations.", + "FSharp.Compiler.PostInferenceChecks.", + ) + ) + for f in frames + ) + ): + frames = frames[:-1] + total += elapsed + # Exclude Core wrappers that are currently executing compiler callbacks. + last_compiler = max(i for i, f in enumerate(frames) if "FSharp.Compiler.Service!" in f) + core_frames = [f for f in frames[last_compiler + 1 :] if f.startswith("FSharp.Core!")] + if core_frames: + core += elapsed + inclusive.update({f: elapsed for f in set(core_frames)}) + if frames[-1].startswith("FSharp.Core!"): + leaf[frames[-1]] += elapsed + first = next( + i for i in range(last_compiler + 1, len(frames)) if frames[i].startswith("FSharp.Core!") + ) + if first: + callers[frames[first - 1]] += elapsed + if event["type"] == "O": + stack.append(event["frame"]) + else: + assert stack.pop() == event["frame"] + previous = event["at"] + result = dict( + trace=path, + sampled_check_ms=total, + core_inclusive_ms=core, + inclusive=inclusive.most_common(25), + leaf=leaf.most_common(25), + callers=callers.most_common(20), + ) + results.append(result) +print(json.dumps(results, indent=2)) diff --git a/notes/benchmarks/core-runtime/api-difference.json b/notes/benchmarks/core-runtime/api-difference.json new file mode 100644 index 0000000000..645e79c390 --- /dev/null +++ b/notes/benchmarks/core-runtime/api-difference.json @@ -0,0 +1,46 @@ +{ + "runtime_package": "FSharp.Core/10.1.203", + "local_source_commit": "c32d52e92", + "missing_or_changed": [ + "MEMBER Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`1 Method Boolean Equals(System.Object)", + "MEMBER Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`1 Method Int32 GetHashCode()", + "MEMBER Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`2 Method Boolean Equals(System.Object)", + "MEMBER Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`2 Method Int32 GetHashCode()", + "MEMBER Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`3 Method Boolean Equals(System.Object)", + "MEMBER Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`3 Method Int32 GetHashCode()", + "MEMBER Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`4 Method Boolean Equals(System.Object)", + "MEMBER Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`4 Method Int32 GetHashCode()", + "MEMBER Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`5 Method Boolean Equals(System.Object)", + "MEMBER Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`5 Method Int32 GetHashCode()", + "MEMBER Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`6 Method Boolean Equals(System.Object)", + "MEMBER Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`6 Method Int32 GetHashCode()", + "MEMBER Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`7 Method Boolean Equals(System.Object)", + "MEMBER Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`7 Method Int32 GetHashCode()", + "MEMBER Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`8 Method Boolean Equals(System.Object)", + "MEMBER Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`8 Method Int32 GetHashCode()", + "TYPE Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`1 Public, Sealed, Serializable, BeforeFieldInit System.Object", + "TYPE Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`2 Public, Sealed, Serializable, BeforeFieldInit System.Object", + "TYPE Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`3 Public, Sealed, Serializable, BeforeFieldInit System.Object", + "TYPE Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`4 Public, Sealed, Serializable, BeforeFieldInit System.Object", + "TYPE Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`5 Public, Sealed, Serializable, BeforeFieldInit System.Object", + "TYPE Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`6 Public, Sealed, Serializable, BeforeFieldInit System.Object", + "TYPE Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`7 Public, Sealed, Serializable, BeforeFieldInit System.Object", + "TYPE Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`8 Public, Sealed, Serializable, BeforeFieldInit System.Object" + ], + "additional_or_changed": [ + "TYPE Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`1 Public, Serializable, BeforeFieldInit System.Object", + "TYPE Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`2 Public, Serializable, BeforeFieldInit System.Object", + "TYPE Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`3 Public, Serializable, BeforeFieldInit System.Object", + "TYPE Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`4 Public, Serializable, BeforeFieldInit System.Object", + "TYPE Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`5 Public, Serializable, BeforeFieldInit System.Object", + "TYPE Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`6 Public, Serializable, BeforeFieldInit System.Object", + "TYPE Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`7 Public, Serializable, BeforeFieldInit System.Object", + "TYPE Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`8 Public, Serializable, BeforeFieldInit System.Object" + ], + "probe": { + "separate_AnonymousObject_int_42_equality": { + "shipped": true, + "local_source": false + } + } +} \ No newline at end of file diff --git a/notes/benchmarks/core-runtime/map-inlining-experiment.patch b/notes/benchmarks/core-runtime/map-inlining-experiment.patch new file mode 100644 index 0000000000..2389584213 --- /dev/null +++ b/notes/benchmarks/core-runtime/map-inlining-experiment.patch @@ -0,0 +1,18 @@ +--- a/src/FSharp.Core/map.fs ++++ b/src/FSharp.Core/map.fs +@@ -104,6 +104,7 @@ + [] + let tolerance = 2 + ++ [] + let mk l k v r : MapTree<'Key, 'Value> = + let hl = height l + let hr = height r +@@ -114,6 +115,7 @@ + else + MapTreeNode(k, v, l, r, m + 1) :> MapTree<'Key, 'Value> // new map is higher by 1 than the highest + ++ [] + let rebalance t1 (k: 'Key) (v: 'Value) t2 : MapTree<'Key, 'Value> = + let t1h = height t1 + let t2h = height t2 diff --git a/notes/benchmarks/core-runtime/prepare-aligned-core.py b/notes/benchmarks/core-runtime/prepare-aligned-core.py new file mode 100644 index 0000000000..7b5f0889a3 --- /dev/null +++ b/notes/benchmarks/core-runtime/prepare-aligned-core.py @@ -0,0 +1,102 @@ +"""Build and check an isolated Core baseline; never replaces the shipped runtime.""" + +import argparse +import hashlib +import json +import os +from pathlib import Path +import shutil +import subprocess + +ROOT = Path(__file__).resolve().parents[3] +FCS_COMMIT = "7d0437e8bcf9ead9483483eaa556f1998af8806b" +VMR_COMMIT = "c23858a6d860abe3ca94f9ac630c3e61b2625664" +parser = argparse.ArgumentParser(description=__doc__) +parser.add_argument("--source", type=Path, default=ROOT / "temp/core-runtime-source") +parser.add_argument( + "--runtime", + type=Path, + default=ROOT / "temp/release-0.5.3/verified-package/tools/net10.0/win-x64/FSharp.Core.dll", +) +args = parser.parse_args() +source = args.source.resolve() +runtime = args.runtime.resolve() +output = ROOT / "temp/core-runtime-baseline" +output.mkdir(parents=True, exist_ok=True) + + +def run(*command, cwd=ROOT, capture=False, env=None): + result = subprocess.run( + ["rtk", "proxy", *map(str, command)], + cwd=cwd, + env=env, + check=True, + text=True, + capture_output=capture, + ) + return result.stdout.strip() if capture else None + + +if not runtime.is_file(): + raise FileNotFoundError(runtime) +if not source.exists(): + run("git", "clone", "--filter=blob:none", "--no-checkout", "https://github.com/dotnet/fsharp.git", source) + run("git", "checkout", "--detach", FCS_COMMIT, cwd=source) +if run("git", "rev-parse", "HEAD", cwd=source, capture=True) != FCS_COMMIT: + raise RuntimeError("Source checkout is not the pinned runtime source commit") +if run("git", "status", "--porcelain", cwd=source, capture=True): + raise RuntimeError("Baseline build requires an unchanged source checkout") + +# Only the public key is extracted. Public signing is for this .NET experiment; +# it is not a Microsoft signature or authorization to publish a replacement. +key = output / "runtime-public-key.snk" +env = dict(os.environ, FICKL_CORE_INPUT=str(runtime), FICKL_CORE_KEY_OUT=str(key)) +run( + "powershell", + "-NoProfile", + "-Command", + "$name = [Reflection.AssemblyName]::GetAssemblyName($env:FICKL_CORE_INPUT); " + "if ($name.FullName -ne 'FSharp.Core, Version=10.1.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a') " + "{ throw 'Unexpected runtime assembly identity' }; " + "[IO.File]::WriteAllBytes($env:FICKL_CORE_KEY_OUT, $name.GetPublicKey())", + env=env, +) +run( + "dotnet", + "build", + "src/FSharp.Core/FSharp.Core.fsproj", + "-c", + "Release", + "-f", + "netstandard2.1", + "-p:BUILDING_USING_DOTNET=true", + "-p:AssemblyVersion=10.1.0.0", + "-p:SignAssembly=true", + "-p:PublicSign=true", + f"-p:AssemblyOriginatorKeyFile={key}", + "-p:NuGetAudit=false", + "-v:q", + cwd=source, +) +built = source / "artifacts/bin/FSharp.Core/Release/netstandard2.1/FSharp.Core.dll" +baseline = output / "FSharp.Core.dll" +shutil.copy2(built, baseline) +project = Path(__file__).with_name("Surface.csproj") +run("dotnet", "build", project, "-c", "Release", "-v:q") +probe = project.parent / "bin/Release/net10.0/Surface.dll" +for label, assembly in [("runtime", runtime), ("baseline", baseline)]: + run("dotnet", probe, assembly, output / f"{label}-api.txt", "true") +if (output / "runtime-api.txt").read_text().splitlines() != (output / "baseline-api.txt").read_text().splitlines(): + raise RuntimeError("Reflected API differs from the shipped runtime") +metadata = dict( + source_commit=FCS_COMMIT, + dotnet_source_commit=VMR_COMMIT, + runtime_package="FSharp.Core/10.1.203", + target_framework="netstandard2.1", + public_signed=True, + runtime_sha256=hashlib.sha256(runtime.read_bytes()).hexdigest(), + baseline_sha256=hashlib.sha256(baseline.read_bytes()).hexdigest(), + api_and_anonymous_object_equality_match=True, +) +(output / "manifest.json").write_text(json.dumps(metadata, indent=2)) +print(f"Verified isolated baseline: {baseline}") diff --git a/notes/benchmarks/core-runtime/profile-results.json b/notes/benchmarks/core-runtime/profile-results.json new file mode 100644 index 0000000000..755fc1defb --- /dev/null +++ b/notes/benchmarks/core-runtime/profile-results.json @@ -0,0 +1,1422 @@ +[ + { + "trace": "temp/core-profile/binding-1.speedscope.json", + "sampled_check_ms": 1208.1846427370524, + "core_inclusive_ms": 379.0841250739213, + "inclusive": [ + [ + "FSharp.Core!Microsoft.FSharp.Collections.MapTreeModule.add", + 143.65248368045604 + ], + [ + "FSharp.Core!Microsoft.FSharp.Collections.MapTreeModule.mk", + 135.15696580449094 + ], + [ + "FSharp.Core!Microsoft.FSharp.Collections.FSharpMap`2[System.__Canon,System.__Canon].Add", + 33.931391630938606 + ], + [ + "FSharp.Core!Microsoft.FSharp.Collections.SeqModule.ToList", + 33.6991983999892 + ], + [ + "FSharp.Core!Microsoft.FSharp.Collections.ListModule.FoldBack", + 30.401890382303918 + ], + [ + "FSharp.Core!Microsoft.FSharp.Primitives.Basics.List.map", + 25.946894441577342 + ], + [ + "FSharp.Core!Microsoft.FSharp.Primitives.Basics.List.zip", + 17.305899144225805 + ], + [ + "FSharp.Core!Microsoft.FSharp.Primitives.Basics.List.mapToFreshConsTail", + 13.76509958808515 + ], + [ + "FSharp.Core!Microsoft.FSharp.Primitives.Basics.List.map2", + 13.254399213236866 + ], + [ + "FSharp.Core!Microsoft.FSharp.Primitives.Basics.List.filter", + 12.377099827157963 + ], + [ + "FSharp.Core!Microsoft.FSharp.Core.Operators.op_Append", + 11.772899438155264 + ], + [ + "FSharp.Core!Microsoft.FSharp.Primitives.Basics.List.filterToFreshConsTail", + 9.806099830627772 + ], + [ + "FSharp.Core!Microsoft.FSharp.Collections.SetTreeModule.add", + 7.723396279591725 + ], + [ + "FSharp.Core!Microsoft.FSharp.Primitives.Basics.List.map2ToFreshConsTail", + 6.862299403849647 + ], + [ + "FSharp.Core!Microsoft.FSharp.Collections.MapTreeModule.foldBackOpt", + 6.580299945062052 + ], + [ + "FSharp.Core!Microsoft.FSharp.Collections.SetTreeModule.mk", + 6.564099859639555 + ], + [ + "FSharp.Core!Microsoft.FSharp.Primitives.Basics.List.choose", + 6.337999368785177 + ], + [ + "FSharp.Core!Microsoft.FSharp.Collections.ListModule.Fold", + 6.332399835889873 + ], + [ + "FSharp.Core!Microsoft.FSharp.Collections.SeqModule+revamp@726[System.__Canon,System.__Canon].Invoke", + 6.11989964215627 + ], + [ + "FSharp.Core!Microsoft.FSharp.Collections.MapTreeModule.rebalance", + 5.337579854752676 + ], + [ + "FSharp.Core!Microsoft.FSharp.Primitives.Basics.Array.stableSortWithKeysAndComparer", + 5.18599986467143 + ], + [ + "FSharp.Core!Microsoft.FSharp.Primitives.Basics.Array.stableSortInPlaceBy", + 5.18599986467143 + ], + [ + "FSharp.Core!Microsoft.FSharp.Primitives.Basics.List.mapFold", + 4.973799695449088 + ], + [ + "FSharp.Core!Microsoft.FSharp.Collections.FSharpMap`2[System.__Canon,System.__Canon].Create", + 4.950499788860725 + ], + [ + "FSharp.Core!Microsoft.FSharp.Collections.MapTreeModule.ofSeq", + 4.8725997949522935 + ] + ], + "leaf": [ + [ + "FSharp.Core!Microsoft.FSharp.Collections.MapTreeModule.mk", + 135.15696580449094 + ], + [ + "FSharp.Core!Microsoft.FSharp.Collections.SeqModule.ToList", + 32.319598409897026 + ], + [ + "FSharp.Core!Microsoft.FSharp.Collections.MapTreeModule.add", + 24.430296858213296 + ], + [ + "FSharp.Core!Microsoft.FSharp.Primitives.Basics.List.mapToFreshConsTail", + 13.450499709368387 + ], + [ + "FSharp.Core!Microsoft.FSharp.Primitives.Basics.List.zip", + 13.135699492611366 + ], + [ + "FSharp.Core!Microsoft.FSharp.Primitives.Basics.List.map", + 11.631594988628876 + ], + [ + "FSharp.Core!Microsoft.FSharp.Primitives.Basics.List.filterToFreshConsTail", + 9.806099830627772 + ], + [ + "FSharp.Core!Microsoft.FSharp.Core.Operators.op_Append", + 7.602899778485153 + ], + [ + "FSharp.Core!Microsoft.FSharp.Collections.SetTreeModule.mk", + 6.564099859639555 + ], + [ + "FSharp.Core!Microsoft.FSharp.Primitives.Basics.List.map2", + 5.356099856994206 + ], + [ + "FSharp.Core!Microsoft.FSharp.Primitives.Basics.List.choose", + 5.296899392197702 + ], + [ + "FSharp.Core!Microsoft.FSharp.Primitives.Basics.List.mapFold", + 4.71289970098951 + ], + [ + "FSharp.Core!Microsoft.FSharp.Primitives.Basics.List.zipToFreshConsTail", + 4.170199651614439 + ], + [ + "FSharp.Core!Microsoft.FSharp.Collections.PrivateListHelpers.appendToFreshConsTail", + 4.169999659670111 + ], + [ + "FSharp.Core!Microsoft.FSharp.Collections.SetTreeModule.add", + 4.033096308571203 + ], + [ + "FSharp.Core!Microsoft.FSharp.Collections.FSharpMap`2[System.__Canon,System.__Canon].Add", + 4.032499936892691 + ], + [ + "FSharp.Core!Microsoft.FSharp.Primitives.Basics.List.toArray", + 3.7100994460176935 + ], + [ + "FSharp.Core!Microsoft.FSharp.Primitives.Basics.List.appendToFreshConsTail", + 3.327895674460933 + ], + [ + "FSharp.Core!Microsoft.FSharp.Collections.FSharpList`1[System.__Canon].System.Collections.Generic.IEnumerable<'T>.GetEnumerator", + 3.2652997344011965 + ], + [ + "FSharp.Core!Microsoft.FSharp.Core.OptimizedClosures+FSharpFunc`3[System.__Canon,System.__Canon,System.__Canon].Invoke", + 3.205499971367317 + ], + [ + "FSharp.Core!Microsoft.FSharp.Core.FSharpOption`1[System.__Canon].Some", + 2.834498608823651 + ], + [ + "FSharp.Core!Microsoft.FSharp.Primitives.Basics.List.map2ToFreshConsTail", + 2.6453997367116244 + ], + [ + "FSharp.Core!Microsoft.FSharp.Core.OptimizedClosures+FSharpFunc`3[System.__Canon,System.__Canon,System.__Canon].Adapt", + 2.5259984011295273 + ], + [ + "FSharp.Core!Microsoft.FSharp.Primitives.Basics.List.ofArray", + 2.428999939477535 + ], + [ + "FSharp.Core!Microsoft.FSharp.Collections.MapTreeModule.mapiOpt", + 2.3247998710103275 + ] + ], + "callers": [ + [ + "FSharp.Compiler.Service!FSharp.Compiler.TypedTree+addEntityByMangledName@2090.Invoke", + 100.24297168907492 + ], + [ + "FSharp.Compiler.Service!FSharp.Compiler.PostTypeCheckSemanticChecks.CheckDefnInModule", + 30.7964983693837 + ], + [ + "FSharp.Compiler.Service!FSharp.Compiler.TypeHierarchy.loop@233-46", + 23.05939592664845 + ], + [ + "FSharp.Compiler.Service!FSharp.Compiler.TypedTreeOps.GetTopTauTypeInFSharpForm", + 12.118499240885285 + ], + [ + "FSharp.Compiler.Service!FSharp.Compiler.InfoReader.GetImmediateIntrinsicMethInfosOfTypeAux", + 10.00999988938247 + ], + [ + "FSharp.Compiler.Service!FSharp.Compiler.TypedTree+ModuleOrNamespaceType.get_AllValsAndMembersByPartialLinkageKey", + 9.00169371992888 + ], + [ + "FSharp.Compiler.Service!FSharp.Compiler.InfoReader+IndexedList`1[System.__Canon].AddItems", + 7.323199672177907 + ], + [ + "FSharp.Compiler.Service!FSharp.Compiler.InfoReader+clo@433-298.Invoke", + 7.023999907286907 + ], + [ + "FSharp.Compiler.Service!FSharp.Compiler.TypedTreeOps.stripTyEqnsA", + 6.871599649811287 + ], + [ + "FSharp.Compiler.Service!FSharp.Compiler.NameResolution.AddTyconRefsToNameEnv", + 5.341299766731709 + ], + [ + "FSharp.Compiler.Service!Internal.Utilities.Library.NameMultiMap+chooseRange@1263[System.__Canon,System.__Canon].Invoke", + 5.138299417010785 + ], + [ + "FSharp.Compiler.Service!FSharp.Compiler.NameResolution.ResolveObjectConstructorPrim", + 4.919699880138978 + ], + [ + "FSharp.Compiler.Service!FSharp.Compiler.MethodCalls+CalledMeth`1[System.__Canon]..ctor", + 4.87539991176709 + ], + [ + "FSharp.Compiler.Service!FSharp.Compiler.TypedTree+ModuleOrNamespaceType.get_TypesByMangledName", + 4.720499360888425 + ], + [ + "FSharp.Compiler.Service!FSharp.Compiler.CheckExpressions.PublishArguments", + 4.047099117618927 + ], + [ + "FSharp.Compiler.Service!Internal.Utilities.TypeHashing+StructuralUtilities+TypeStructure.GetHashCode", + 3.657699920665891 + ], + [ + "FSharp.Compiler.Service!FSharp.Compiler.TypedTreeOps.addFreeInModuleTy", + 3.3038992623696686 + ], + [ + "FSharp.Compiler.Service!FSharp.Compiler.NameResolution+AddTyconByAccessNames@865.Invoke", + 3.1904998609188624 + ], + [ + "FSharp.Compiler.Service!FSharp.Compiler.TypedTreeOps+allValsOfModDef@6450.GenerateNext", + 2.985500034486904 + ], + [ + "FSharp.Compiler.Service!Internal.Utilities.Collections.QueueList.foldBack", + 2.913298859328279 + ] + ], + "allocations": { + "trace": "temp/core-profile/binding-1.nettrace", + "ticks": 12692, + "missingStacks": 0, + "eventsLost": 0, + "totalSampledBytes": 1369289664, + "checkingSampledBytes": 1132151472, + "selected": [ + { + "name": "Map nodes under entity-name table construction", + "estimatedMB": 166.44747924804688 + }, + { + "name": "TypeToken boxing under array hashing", + "estimatedMB": 1.3201370239257812 + }, + { + "name": "TypeToken boxing under array equality", + "estimatedMB": 1.2147979736328125 + } + ], + "types": [ + { + "name": "Microsoft.FSharp.Collections.MapTreeNode`2[System.String,FSharp.Compiler.TypedTree+Entity]", + "estimatedMB": 167.46409606933594 + }, + { + "name": "Microsoft.FSharp.Collections.FSharpList`1[FSharp.Compiler.TypedTree+TType]", + "estimatedMB": 38.102684020996094 + }, + { + "name": "Microsoft.FSharp.Collections.FSharpList`1[FSharp.Compiler.Infos+MethInfo]", + "estimatedMB": 27.51305389404297 + }, + { + "name": "System.Tuple`4[Microsoft.FSharp.Collections.FSharpList`1[FSharp.Compiler.TypedTree+Typar],Microsoft.FSharp.Collections.FSharpList`1[Microsoft.FSharp.Collections.FSharpList`1[System.Tuple`2[FSharp.Compiler.TypedTree+TType,FSharp.Compiler.TypedTree+ArgReprInfo]]],FSharp.Compiler.TypedTree+TType,FSharp.Compiler.TypedTree+ArgReprInfo]", + "estimatedMB": 22.868865966796875 + }, + { + "name": "TType_app", + "estimatedMB": 20.72332000732422 + }, + { + "name": "System.Tuple`2[FSharp.Compiler.TypedTree+TType,FSharp.Compiler.TypedTree+ArgReprInfo]", + "estimatedMB": 19.009361267089844 + }, + { + "name": "System.Tuple`2[Microsoft.FSharp.Collections.FSharpList`1[FSharp.Compiler.TypedTree+TType],FSharp.Compiler.TypedTree+TType]", + "estimatedMB": 18.396697998046875 + }, + { + "name": "Microsoft.FSharp.Collections.FSharpMap`2[System.String,FSharp.Compiler.TypedTree+Entity]", + "estimatedMB": 15.836067199707031 + }, + { + "name": "Microsoft.FSharp.Collections.FSharpList`1[Microsoft.FSharp.Collections.FSharpList`1[System.Tuple`2[FSharp.Compiler.TypedTree+TType,FSharp.Compiler.TypedTree+ArgReprInfo]]]", + "estimatedMB": 15.443016052246094 + }, + { + "name": "Microsoft.FSharp.Collections.MapTreeNode`2[System.Int64,FSharp.Compiler.TypedTree+ValRef]", + "estimatedMB": 14.429847717285156 + } + ] + } + }, + { + "trace": "temp/core-profile/binding-2.speedscope.json", + "sampled_check_ms": 1286.0530778489808, + "core_inclusive_ms": 421.33399304339787, + "inclusive": [ + [ + "FSharp.Core!Microsoft.FSharp.Collections.MapTreeModule.add", + 158.8813844943145 + ], + [ + "FSharp.Core!Microsoft.FSharp.Collections.MapTreeModule.mk", + 153.04348557205662 + ], + [ + "FSharp.Core!Microsoft.FSharp.Primitives.Basics.List.map2", + 34.02549861235411 + ], + [ + "FSharp.Core!Microsoft.FSharp.Collections.FSharpMap`2[System.__Canon,System.__Canon].Add", + 33.74258881461492 + ], + [ + "FSharp.Core!Microsoft.FSharp.Collections.ListModule.FoldBack", + 32.299788182053135 + ], + [ + "FSharp.Core!Microsoft.FSharp.Primitives.Basics.List.map", + 30.549965146614113 + ], + [ + "FSharp.Core!Microsoft.FSharp.Primitives.Basics.List.map2ToFreshConsTail", + 22.694399795398112 + ], + [ + "FSharp.Core!Microsoft.FSharp.Collections.SeqModule+revamp@726[System.__Canon,System.__Canon].Invoke", + 16.56819905485736 + ], + [ + "FSharp.Core!Microsoft.FSharp.Primitives.Basics.List.mapToFreshConsTail", + 15.548069614537098 + ], + [ + "FSharp.Core!Microsoft.FSharp.Core.Operators.op_Append", + 13.475499673725835 + ], + [ + "FSharp.Core!Microsoft.FSharp.Primitives.Basics.List.choose", + 13.43439816514865 + ], + [ + "FSharp.Core!Microsoft.FSharp.Primitives.Basics.List.filter", + 12.922998551206092 + ], + [ + "FSharp.Core!Microsoft.FSharp.Primitives.Basics.List.zip", + 12.867999188244994 + ], + [ + "FSharp.Core!Microsoft.FSharp.Core.CompilerServices.RuntimeHelpers+mkConcatSeq@348[System.__Canon,System.__Canon].Invoke", + 12.606499525714298 + ], + [ + "FSharp.Core!Microsoft.FSharp.Core.CompilerServices.RuntimeHelpers+ConcatEnumerator`2[System.__Canon,System.__Canon]..ctor", + 12.233999545258484 + ], + [ + "FSharp.Core!Microsoft.FSharp.Collections.SetTreeModule.add", + 12.217848022200542 + ], + [ + "FSharp.Core!Microsoft.FSharp.Collections.MapTreeModule.foldBackOpt", + 11.693399946589352 + ], + [ + "FSharp.Core!Microsoft.FSharp.Collections.SeqModule+Map2@763[System.__Canon,System.__Canon,System.__Canon].Invoke", + 11.36369978042876 + ], + [ + "FSharp.Core!Microsoft.FSharp.Primitives.Basics.List.filterToFreshConsTail", + 11.023399557862376 + ], + [ + "FSharp.Core!Microsoft.FSharp.Collections.ArrayModule.Fold", + 7.879799076050631 + ], + [ + "FSharp.Core!Microsoft.FSharp.Collections.MapTreeModule.rebalance", + 7.817299022344969 + ], + [ + "FSharp.Core!Microsoft.FSharp.Collections.MapTreeModule.ofSeq", + 6.3833996963549 + ], + [ + "FSharp.Core!Microsoft.FSharp.Collections.FSharpMap`2[System.__Canon,System.__Canon].Create", + 6.3833996963549 + ], + [ + "FSharp.Core!Microsoft.FSharp.Primitives.Basics.List.collect", + 6.1199988497169215 + ], + [ + "FSharp.Core!Microsoft.FSharp.Collections.SetTreeModule.mk", + 5.844548182027893 + ] + ], + "leaf": [ + [ + "FSharp.Core!Microsoft.FSharp.Collections.MapTreeModule.mk", + 153.04348557205662 + ], + [ + "FSharp.Core!Microsoft.FSharp.Collections.MapTreeModule.add", + 21.7544909433675 + ], + [ + "FSharp.Core!Microsoft.FSharp.Primitives.Basics.List.map2ToFreshConsTail", + 18.660000269317152 + ], + [ + "FSharp.Core!Microsoft.FSharp.Primitives.Basics.List.map", + 14.15599554010305 + ], + [ + "FSharp.Core!Microsoft.FSharp.Primitives.Basics.List.mapToFreshConsTail", + 13.935769675158099 + ], + [ + "FSharp.Core!Microsoft.FSharp.Collections.SeqModule+Map2@763[System.__Canon,System.__Canon,System.__Canon].Invoke", + 11.36369978042876 + ], + [ + "FSharp.Core!Microsoft.FSharp.Primitives.Basics.List.filterToFreshConsTail", + 10.996799562899014 + ], + [ + "FSharp.Core!Microsoft.FSharp.Primitives.Basics.List.choose", + 10.369298206484473 + ], + [ + "FSharp.Core!Microsoft.FSharp.Core.Operators.op_Append", + 9.488299969066247 + ], + [ + "FSharp.Core!Microsoft.FSharp.Collections.SetTreeModule.add", + 9.061699796339099 + ], + [ + "FSharp.Core!Microsoft.FSharp.Primitives.Basics.List.zip", + 8.562099375110392 + ], + [ + "FSharp.Core!Microsoft.FSharp.Primitives.Basics.List.map2", + 6.779798867858517 + ], + [ + "FSharp.Core!Microsoft.FSharp.Collections.SetTreeModule.mk", + 5.844548182027893 + ], + [ + "FSharp.Core!Microsoft.FSharp.Collections.FSharpList`1[System.__Canon].Cons", + 5.079399194379903 + ], + [ + "FSharp.Core!Microsoft.FSharp.Primitives.Basics.List.appendToFreshConsTail", + 4.7306988634918525 + ], + [ + "FSharp.Core!Microsoft.FSharp.Primitives.Basics.List.zipToFreshConsTail", + 4.305899813134602 + ], + [ + "FSharp.Core!Microsoft.FSharp.Collections.SeqModule.ToList", + 4.139399224877252 + ], + [ + "FSharp.Core!Microsoft.FSharp.Core.OptimizedClosures+FSharpFunc`3[System.__Canon,System.__Canon,System.__Canon].Adapt", + 4.091899820733033 + ], + [ + "FSharp.Core!Microsoft.FSharp.Core.FSharpOption`1[System.__Canon].Some", + 4.040699861610165 + ], + [ + "FSharp.Core!Microsoft.FSharp.Collections.PrivateListHelpers.appendToFreshConsTail", + 3.987199704659588 + ], + [ + "FSharp.Core!Microsoft.FSharp.Primitives.Basics.List.ofArray", + 3.8736999408483825 + ], + [ + "FSharp.Core!Microsoft.FSharp.Primitives.Basics.List.exists", + 3.584599935864844 + ], + [ + "FSharp.Core!Microsoft.FSharp.Collections.MapTreeModule.tryGetValue", + 3.467799946665764 + ], + [ + "FSharp.Core!Microsoft.FSharp.Primitives.Basics.List.toArray", + 3.4280958776530497 + ], + [ + "FSharp.Core!Microsoft.FSharp.Core.OptimizedClosures+FSharpFunc`3[System.__Canon,System.__Canon,System.__Canon].Invoke", + 3.4202998399255193 + ] + ], + "callers": [ + [ + "FSharp.Compiler.Service!FSharp.Compiler.TypedTree+addEntityByMangledName@2090.Invoke", + 111.20358635916159 + ], + [ + "FSharp.Compiler.Service!FSharp.Compiler.TypedTreeOps.GetTopTauTypeInFSharpForm", + 30.256298674475147 + ], + [ + "FSharp.Compiler.Service!FSharp.Compiler.TypeHierarchy.loop@233-46", + 25.78574738708903 + ], + [ + "FSharp.Compiler.Service!FSharp.Compiler.CheckExpressions.PublishArguments", + 15.353999387562908 + ], + [ + "FSharp.Compiler.Service!FSharp.Compiler.InfoReader.GetImmediateIntrinsicMethInfosOfTypeAux", + 12.969599873548304 + ], + [ + "FSharp.Compiler.Service!FSharp.Compiler.TypedTree+ModuleOrNamespaceType.get_AllValsAndMembersByPartialLinkageKey", + 9.16819432722923 + ], + [ + "FSharp.Compiler.Service!Internal.Utilities.Library.NameMultiMap+chooseRange@1263[System.__Canon,System.__Canon].Invoke", + 8.565198281807625 + ], + [ + "FSharp.Compiler.Service!FSharp.Compiler.InfoReader+IndexedList`1[System.__Canon].AddItems", + 8.376599622498361 + ], + [ + "FSharp.Compiler.Service!FSharp.Compiler.InfoReader+clo@433-298.Invoke", + 7.342399852053859 + ], + [ + "FSharp.Compiler.Service!FSharp.Compiler.NameResolution.AddTyconRefsToNameEnv", + 4.319199113280092 + ], + [ + "FSharp.Compiler.Service!Internal.Utilities.Collections.QueueList.foldBack", + 4.065099396013466 + ], + [ + "FSharp.Compiler.Service!FSharp.Compiler.TypeHierarchy.GetImmediateInterfacesOfMetadataType", + 3.9537998715936737 + ], + [ + "FSharp.Compiler.Service!FSharp.Compiler.NameResolution+AddTyconByAccessNames@865.Invoke", + 3.9351998588881543 + ], + [ + "FSharp.Compiler.Service!FSharp.Compiler.TypedTreeOps.stripTyEqnsA", + 3.9086998408652107 + ], + [ + "FSharp.Compiler.Service!Internal.Utilities.Library.Extras+Graph`2[System.__Canon,System.Int64]..ctor", + 3.8414999183937653 + ], + [ + "FSharp.Compiler.Service!FSharp.Compiler.MethodOverrides+DispatchSlotChecking.CheckDispatchSlotsAreImplemented", + 3.761199771957763 + ], + [ + "FSharp.Compiler.Service!FSharp.Compiler.CheckDeclarations+TyconConstraintInference.InferSetOfTyconsSupportingEquatable", + 3.597199384689702 + ], + [ + "FSharp.Compiler.Service!Internal.Utilities.Library.MultiMap.find", + 3.3710999488830566 + ], + [ + "FSharp.Compiler.Service!FSharp.Compiler.TypedTreeOps+OfList@100-3[System.__Canon].Invoke", + 2.979699859901757 + ], + [ + "FSharp.Compiler.Service!FSharp.Compiler.NameResolution.ResolveObjectConstructorPrim", + 2.9326999290042295 + ] + ], + "allocations": { + "trace": "temp/core-profile/binding-2.nettrace", + "ticks": 12697, + "missingStacks": 0, + "eventsLost": 0, + "totalSampledBytes": 1369926552, + "checkingSampledBytes": 1133264872, + "selected": [ + { + "name": "Map nodes under entity-name table construction", + "estimatedMB": 166.0443572998047 + }, + { + "name": "TypeToken boxing under array hashing", + "estimatedMB": 1.217498779296875 + }, + { + "name": "TypeToken boxing under array equality", + "estimatedMB": 0.6132278442382812 + } + ], + "types": [ + { + "name": "Microsoft.FSharp.Collections.MapTreeNode`2[System.String,FSharp.Compiler.TypedTree+Entity]", + "estimatedMB": 166.75598907470703 + }, + { + "name": "Microsoft.FSharp.Collections.FSharpList`1[FSharp.Compiler.TypedTree+TType]", + "estimatedMB": 37.475860595703125 + }, + { + "name": "Microsoft.FSharp.Collections.FSharpList`1[FSharp.Compiler.Infos+MethInfo]", + "estimatedMB": 29.033035278320312 + }, + { + "name": "TType_app", + "estimatedMB": 24.575180053710938 + }, + { + "name": "System.Tuple`2[Microsoft.FSharp.Collections.FSharpList`1[FSharp.Compiler.TypedTree+TType],FSharp.Compiler.TypedTree+TType]", + "estimatedMB": 19.207077026367188 + }, + { + "name": "System.Tuple`4[Microsoft.FSharp.Collections.FSharpList`1[FSharp.Compiler.TypedTree+Typar],Microsoft.FSharp.Collections.FSharpList`1[Microsoft.FSharp.Collections.FSharpList`1[System.Tuple`2[FSharp.Compiler.TypedTree+TType,FSharp.Compiler.TypedTree+ArgReprInfo]]],FSharp.Compiler.TypedTree+TType,FSharp.Compiler.TypedTree+ArgReprInfo]", + "estimatedMB": 18.697891235351562 + }, + { + "name": "Microsoft.FSharp.Collections.FSharpList`1[System.Tuple`2[FSharp.Compiler.TypedTree+TType,FSharp.Compiler.TypedTree+ArgReprInfo]]", + "estimatedMB": 16.773605346679688 + }, + { + "name": "Microsoft.FSharp.Collections.FSharpMap`2[System.String,FSharp.Compiler.TypedTree+Entity]", + "estimatedMB": 15.550086975097656 + }, + { + "name": "Microsoft.FSharp.Collections.FSharpList`1[Microsoft.FSharp.Collections.FSharpList`1[System.Tuple`2[FSharp.Compiler.TypedTree+TType,FSharp.Compiler.TypedTree+ArgReprInfo]]]", + "estimatedMB": 15.546333312988281 + }, + { + "name": "System.Tuple`2[FSharp.Compiler.TypedTree+TType,FSharp.Compiler.TypedTree+ArgReprInfo]", + "estimatedMB": 15.134849548339844 + } + ] + } + }, + { + "trace": "temp/core-profile/js-1.speedscope.json", + "sampled_check_ms": 14977.691974704958, + "core_inclusive_ms": 3241.7098442886745, + "inclusive": [ + [ + "FSharp.Core!Microsoft.FSharp.Core.LanguagePrimitives+HashCompare.GenericHashArbArray", + 318.5735941078592 + ], + [ + "FSharp.Core!Microsoft.FSharp.Primitives.Basics.List.map", + 278.50758409627724 + ], + [ + "FSharp.Core!Microsoft.FSharp.Collections.MapTreeModule.add", + 237.7560636980993 + ], + [ + "FSharp.Core!Microsoft.FSharp.Primitives.Basics.List.zip", + 211.2907963554958 + ], + [ + "FSharp.Core!Microsoft.FSharp.Collections.FSharpList`1[System.__Canon].Cons", + 196.39419567187906 + ], + [ + "FSharp.Core!Microsoft.FSharp.Core.LanguagePrimitives+HashCompare.GenericEqualityArbArray", + 195.10229739444094 + ], + [ + "FSharp.Core!Microsoft.FSharp.Collections.SeqModule.ToArray", + 187.1279955554669 + ], + [ + "FSharp.Core!Microsoft.FSharp.Collections.MapTreeModule.mk", + 186.91345378959977 + ], + [ + "FSharp.Core!Microsoft.FSharp.Core.LanguagePrimitives+HashCompare.check@1623-2", + 185.5726975403404 + ], + [ + "FSharp.Core!Microsoft.FSharp.Collections.FSharpMap`2[System.__Canon,System.__Canon].Add", + 144.85646738609148 + ], + [ + "FSharp.Core!Microsoft.FSharp.Collections.SeqModule.toArray$cont@1026", + 105.70529618573619 + ], + [ + "FSharp.Core!Microsoft.FSharp.Collections.ListModule.Fold", + 97.9692773233296 + ], + [ + "FSharp.Core!Microsoft.FSharp.Primitives.Basics.Array.stableSortInPlaceBy", + 91.38459929389319 + ], + [ + "FSharp.Core!Microsoft.FSharp.Primitives.Basics.Array.stableSortWithKeysAndComparer", + 90.14799925975194 + ], + [ + "FSharp.Core!Microsoft.FSharp.Collections.ListModule.FoldBack2", + 87.00320022665255 + ], + [ + "FSharp.Core!Microsoft.FSharp.Collections.ListModule.SortBy", + 77.59519954655798 + ], + [ + "FSharp.Core!Microsoft.FSharp.Core.CompilerServices.ListCollector`1[System.__Canon].Add", + 72.97429827791211 + ], + [ + "FSharp.Core!Microsoft.FSharp.Primitives.Basics.List.map2", + 71.73779921582991 + ], + [ + "FSharp.Core!Microsoft.FSharp.Collections.FSharpSet`1[System.Int64].Add", + 68.46159769084761 + ], + [ + "FSharp.Core!Microsoft.FSharp.Primitives.Basics.List.mapToFreshConsTail", + 66.14349858313881 + ], + [ + "FSharp.Core!Microsoft.FSharp.Core.FSharpOption`1[System.__Canon].Some", + 65.98857681830941 + ], + [ + "FSharp.Core!Microsoft.FSharp.Collections.ListModule.FoldBack", + 61.456089719289594 + ], + [ + "FSharp.Core!Microsoft.FSharp.Collections.SeqModule+Truncate@1081[Internal.Utilities.TypeHashing+StructuralUtilities+TypeToken].GetFreshEnumerator", + 61.27999979051492 + ], + [ + "FSharp.Core!Microsoft.FSharp.Collections.FSharpList`1[System.__Canon].System.Collections.Generic.IEnumerable<'T>.GetEnumerator", + 53.35869874832406 + ], + [ + "FSharp.Core!Microsoft.FSharp.Primitives.Basics.List.filter", + 50.05509859929316 + ] + ], + "leaf": [ + [ + "FSharp.Core!Microsoft.FSharp.Collections.FSharpList`1[System.__Canon].Cons", + 196.39419567187906 + ], + [ + "FSharp.Core!Microsoft.FSharp.Collections.MapTreeModule.mk", + 186.91345378959977 + ], + [ + "FSharp.Core!Microsoft.FSharp.Primitives.Basics.List.map", + 178.68938675278332 + ], + [ + "FSharp.Core!Microsoft.FSharp.Primitives.Basics.List.zip", + 176.6361972102295 + ], + [ + "FSharp.Core!Microsoft.FSharp.Collections.SeqModule.toArray$cont@1026", + 99.28279642469079 + ], + [ + "FSharp.Core!Microsoft.FSharp.Collections.ListModule.Fold", + 92.68799782226142 + ], + [ + "FSharp.Core!Microsoft.FSharp.Collections.MapTreeModule.add", + 80.2456865028571 + ], + [ + "FSharp.Core!Microsoft.FSharp.Core.CompilerServices.ListCollector`1[System.__Canon].Add", + 72.97429827791211 + ], + [ + "FSharp.Core!Microsoft.FSharp.Core.FSharpOption`1[System.__Canon].Some", + 65.98857681830941 + ], + [ + "FSharp.Core!Microsoft.FSharp.Collections.SeqModule+Truncate@1081[Internal.Utilities.TypeHashing+StructuralUtilities+TypeToken].GetFreshEnumerator", + 61.27999979051492 + ], + [ + "FSharp.Core!Microsoft.FSharp.Primitives.Basics.List.mapToFreshConsTail", + 61.129198664999876 + ], + [ + "FSharp.Core!Microsoft.FSharp.Collections.FSharpList`1[System.__Canon].System.Collections.Generic.IEnumerable<'T>.GetEnumerator", + 53.35869874832406 + ], + [ + "FSharp.Core!Microsoft.FSharp.Primitives.Basics.List.exists", + 45.758197967433716 + ], + [ + "FSharp.Core!Microsoft.FSharp.Collections.SeqModule.Truncate", + 45.06209933171067 + ], + [ + "FSharp.Core!Microsoft.FSharp.Collections.PrivateListHelpers.appendToFreshConsTail", + 32.64797915709096 + ], + [ + "FSharp.Core!Microsoft.FSharp.Primitives.Basics.List.mapFold", + 30.670898888080956 + ], + [ + "FSharp.Core!Microsoft.FSharp.Collections.FSharpMap`2[System.__Canon,System.__Canon].Add", + 30.579298845585072 + ], + [ + "FSharp.Core!Microsoft.FSharp.Primitives.Basics.List.zipToFreshConsTail", + 29.329499361177568 + ], + [ + "FSharp.Core!Microsoft.FSharp.Primitives.Basics.List.map2", + 29.055699767711758 + ], + [ + "FSharp.Core!Microsoft.FSharp.Collections.SetTreeModule.add", + 27.624699405334468 + ], + [ + "FSharp.Core!Microsoft.FSharp.Collections.FSharpSet`1[System.Int64].Add", + 25.571698973340062 + ], + [ + "FSharp.Core!Microsoft.FSharp.Collections.SetTreeModule.mk", + 22.908898751999004 + ], + [ + "FSharp.Core!Microsoft.FSharp.Primitives.Basics.List.filterToFreshConsTail", + 22.335399629818767 + ], + [ + "FSharp.Core!Microsoft.FSharp.Primitives.Basics.List.filter", + 22.139199042573637 + ], + [ + "FSharp.Core!Microsoft.FSharp.Primitives.Basics.List.toArray", + 20.76908643986053 + ] + ], + "callers": [ + [ + "FSharp.Compiler.Service!Internal.Utilities.TypeHashing+StructuralUtilities+TypeStructure.GetHashCode", + 318.5735941078592 + ], + [ + "FSharp.Compiler.Service!Internal.Utilities.TypeHashing+StructuralUtilities.getTypeStructureOfStrippedType", + 230.4173948876064 + ], + [ + "FSharp.Compiler.Service!FSharp.Compiler.TypeRelations+TTypeCacheKey.Equals", + 188.3129974920139 + ], + [ + "FSharp.Compiler.Service!FSharp.Compiler.TypedTreeOps.stripTyEqnsA", + 104.42589870282791 + ], + [ + "FSharp.Compiler.Service!FSharp.Compiler.TypeHierarchy.loop@233-46", + 96.96079431720909 + ], + [ + "FSharp.Compiler.Service!FSharp.Compiler.CheckIncrementalClasses.MakeCtorForIncrClassConstructionPhase2C", + 85.3609981443883 + ], + [ + "FSharp.Compiler.Service!FSharp.Compiler.TypedTreeOps.bindLocalVals", + 83.51580017471315 + ], + [ + "FSharp.Compiler.Service!FSharp.Compiler.MethodCalls+CalledMeth`1[System.__Canon]..ctor", + 71.25409894499853 + ], + [ + "FSharp.Compiler.Service!Internal.Utilities.Library.LayeredMultiMap`2[System.__Canon,System.__Canon].Add", + 62.062793114325814 + ], + [ + "FSharp.Compiler.Service!FSharp.Compiler.TypedTreeOps+TyconRefMultiMap`1[System.__Canon].Add", + 47.94078617525474 + ], + [ + "FSharp.Compiler.Service!.$Caches+post@339[FSharp.Compiler.TypeRelations+TTypeCacheKey,System.Boolean].Invoke", + 46.90409873546787 + ], + [ + "FSharp.Compiler.Service!FSharp.Compiler.TypeHierarchy.GetImmediateInterfacesOfMetadataType", + 46.75959827152292 + ], + [ + "FSharp.Compiler.Service!FSharp.Compiler.MethodCalls.AdjustCallerArgs", + 44.9401992248695 + ], + [ + "FSharp.Compiler.Service!FSharp.Compiler.NameResolution.AddValRefToNameEnv", + 43.460699363313324 + ], + [ + "FSharp.Compiler.Service!FSharp.Compiler.TypeHierarchy.GetImmediateInterfacesOfType", + 38.929299221003475 + ], + [ + "FSharp.Compiler.Service!FSharp.Compiler.InfoReader+AddItems@340[System.__Canon].Invoke", + 38.69509726876868 + ], + [ + "FSharp.Compiler.Service!FSharp.Compiler.TypedTreeOps.stripFunTyN", + 37.24709994654813 + ], + [ + "FSharp.Compiler.Service!FSharp.Compiler.TypedTreeBasics.mkNestedNonLocalEntityRef", + 37.23949429549339 + ], + [ + "FSharp.Compiler.Service!FSharp.Compiler.TypedTreeOps.applyForallTy", + 35.987399365770216 + ], + [ + "FSharp.Compiler.Service!FSharp.Compiler.MethodCalls.ExamineMethodForLambdaPropagation", + 28.320999101214966 + ] + ], + "allocations": { + "trace": "temp/core-profile/js-1.nettrace", + "ticks": 77842, + "missingStacks": 1, + "eventsLost": 0, + "totalSampledBytes": 8334499192, + "checkingSampledBytes": 4098897200, + "selected": [ + { + "name": "TypeToken boxing under array equality", + "estimatedMB": 91.07167053222656 + }, + { + "name": "TypeToken boxing under array hashing", + "estimatedMB": 52.83619689941406 + }, + { + "name": "Map nodes under entity-name table construction", + "estimatedMB": 3.1508560180664062 + } + ], + "types": [ + { + "name": "emitTType@443", + "estimatedMB": 283.9960174560547 + }, + { + "name": "TType_app", + "estimatedMB": 195.40137481689453 + }, + { + "name": "TypeToken", + "estimatedMB": 143.90786743164062 + }, + { + "name": "Microsoft.FSharp.Collections.FSharpList`1[FSharp.Compiler.TypedTree+TType]", + "estimatedMB": 108.02005004882812 + }, + { + "name": "System.Tuple`6[System.Int32,System.Int32,System.Int32,FSharp.Compiler.AbstractIL.ILBinaryReader+TaggedIndex`1[FSharp.Compiler.AbstractIL.BinaryConstants+TypeDefOrRefTag],System.Int32,System.Int32]", + "estimatedMB": 107.30239868164062 + }, + { + "name": "System.Tuple`5[System.Int32,System.UInt16,System.UInt16,FSharp.Compiler.AbstractIL.ILBinaryReader+TaggedIndex`1[FSharp.Compiler.AbstractIL.BinaryConstants+TypeOrMethodDefTag],System.Int32]", + "estimatedMB": 103.43575286865234 + }, + { + "name": "emitNullness@418", + "estimatedMB": 99.64474487304688 + }, + { + "name": "ILTypeRef", + "estimatedMB": 60.13648986816406 + }, + { + "name": "TypeToken[]", + "estimatedMB": 55.377601623535156 + }, + { + "name": "ImportILType@304", + "estimatedMB": 46.42084503173828 + } + ] + } + }, + { + "trace": "temp/core-profile/js-2.speedscope.json", + "sampled_check_ms": 18442.8809626989, + "core_inclusive_ms": 4039.167344406349, + "inclusive": [ + [ + "FSharp.Core!Microsoft.FSharp.Primitives.Basics.List.map", + 469.2797816696693 + ], + [ + "FSharp.Core!Microsoft.FSharp.Core.LanguagePrimitives+HashCompare.GenericHashArbArray", + 420.72832146278733 + ], + [ + "FSharp.Core!Microsoft.FSharp.Core.LanguagePrimitives+HashCompare.GenericEqualityArbArray", + 285.62085108778683 + ], + [ + "FSharp.Core!Microsoft.FSharp.Core.LanguagePrimitives+HashCompare.check@1623-2", + 261.0286513814708 + ], + [ + "FSharp.Core!Microsoft.FSharp.Primitives.Basics.List.zip", + 249.86628997868775 + ], + [ + "FSharp.Core!Microsoft.FSharp.Collections.SeqModule.ToArray", + 243.4633934490339 + ], + [ + "FSharp.Core!Microsoft.FSharp.Collections.MapTreeModule.add", + 194.01515136501303 + ], + [ + "FSharp.Core!Microsoft.FSharp.Collections.FSharpList`1[System.__Canon].Cons", + 168.13547585381048 + ], + [ + "FSharp.Core!Microsoft.FSharp.Collections.ListModule.Fold", + 165.43218709566054 + ], + [ + "FSharp.Core!Microsoft.FSharp.Collections.MapTreeModule.mk", + 160.96245236605318 + ], + [ + "FSharp.Core!Microsoft.FSharp.Collections.SeqModule.toArray$cont@1026", + 147.68649500379934 + ], + [ + "FSharp.Core!Microsoft.FSharp.Primitives.Basics.List.exists", + 133.30099402065207 + ], + [ + "FSharp.Core!Microsoft.FSharp.Collections.FSharpMap`2[System.__Canon,System.__Canon].Add", + 120.85925836791239 + ], + [ + "FSharp.Core!Microsoft.FSharp.Primitives.Basics.Array.stableSortWithKeysAndComparer", + 110.10888360346644 + ], + [ + "FSharp.Core!Microsoft.FSharp.Primitives.Basics.Array.stableSortInPlaceBy", + 108.265498593355 + ], + [ + "FSharp.Core!Microsoft.FSharp.Primitives.Basics.List.mapToFreshConsTail", + 105.83909788244046 + ], + [ + "FSharp.Core!Microsoft.FSharp.Collections.ListModule.Map", + 100.26579984273985 + ], + [ + "FSharp.Core!Microsoft.FSharp.Primitives.Basics.List.map2", + 100.1541983287234 + ], + [ + "FSharp.Core!Microsoft.FSharp.Collections.ListModule.SortBy", + 93.72459899022579 + ], + [ + "FSharp.Core!Microsoft.FSharp.Control.Mailbox`1[FSharp.Compiler.Caches.EvictionQueueMessage`2[System.__Canon,System.__Canon]].Post", + 81.96319646931988 + ], + [ + "FSharp.Core!Microsoft.FSharp.Control.FSharpMailboxProcessor`1[FSharp.Compiler.Caches.EvictionQueueMessage`2[System.__Canon,System.__Canon]].Post", + 80.71059645702735 + ], + [ + "FSharp.Core!Microsoft.FSharp.Core.CompilerServices.ListCollector`1[System.__Canon].Add", + 77.46119832397972 + ], + [ + "FSharp.Core!Microsoft.FSharp.Collections.ListModule.FoldBack2", + 76.66400323007156 + ], + [ + "FSharp.Core!Microsoft.FSharp.Collections.FSharpList`1[System.Int32].Cons", + 76.28800012982492 + ], + [ + "FSharp.Core!Microsoft.FSharp.Core.FSharpOption`1[System.__Canon].Some", + 76.11819622728353 + ] + ], + "leaf": [ + [ + "FSharp.Core!Microsoft.FSharp.Primitives.Basics.List.map", + 239.46458613958475 + ], + [ + "FSharp.Core!Microsoft.FSharp.Primitives.Basics.List.zip", + 194.38319122103303 + ], + [ + "FSharp.Core!Microsoft.FSharp.Collections.FSharpList`1[System.__Canon].Cons", + 168.13547585381048 + ], + [ + "FSharp.Core!Microsoft.FSharp.Collections.MapTreeModule.mk", + 160.96245236605318 + ], + [ + "FSharp.Core!Microsoft.FSharp.Collections.ListModule.Fold", + 153.6107023306372 + ], + [ + "FSharp.Core!Microsoft.FSharp.Collections.SeqModule.toArray$cont@1026", + 142.8224950976878 + ], + [ + "FSharp.Core!Microsoft.FSharp.Primitives.Basics.List.exists", + 133.30099402065207 + ], + [ + "FSharp.Core!Microsoft.FSharp.Primitives.Basics.List.mapToFreshConsTail", + 103.16229794069204 + ], + [ + "FSharp.Core!Microsoft.FSharp.Core.CompilerServices.ListCollector`1[System.__Canon].Add", + 77.46119832397972 + ], + [ + "FSharp.Core!Microsoft.FSharp.Collections.FSharpList`1[System.Int32].Cons", + 76.28800012982492 + ], + [ + "FSharp.Core!Microsoft.FSharp.Core.FSharpOption`1[System.__Canon].Some", + 76.11819622728353 + ], + [ + "FSharp.Core!Microsoft.FSharp.Collections.MapTreeModule.add", + 64.2927971426484 + ], + [ + "FSharp.Core!Microsoft.FSharp.Collections.SeqModule+Truncate@1081[Internal.Utilities.TypeHashing+StructuralUtilities+TypeToken].GetFreshEnumerator", + 54.18659922770121 + ], + [ + "FSharp.Core!Microsoft.FSharp.Collections.FSharpList`1[System.__Canon].System.Collections.Generic.IEnumerable<'T>.GetEnumerator", + 54.174497549676744 + ], + [ + "FSharp.Core!Microsoft.FSharp.Primitives.Basics.List.zipToFreshConsTail", + 53.724598798472016 + ], + [ + "FSharp.Core!Microsoft.FSharp.Primitives.Basics.List.mapFold", + 51.45769626698166 + ], + [ + "FSharp.Core!Microsoft.FSharp.Collections.SeqModule.Truncate", + 43.358799487307806 + ], + [ + "FSharp.Core!Microsoft.FSharp.Primitives.Basics.List.filter", + 42.072499214205436 + ], + [ + "FSharp.Core!Microsoft.FSharp.Primitives.Basics.List.choose", + 35.8372978991506 + ], + [ + "FSharp.Core!Microsoft.FSharp.Collections.SeqModule+Truncate@1081[Internal.Utilities.TypeHashing+StructuralUtilities+TypeToken].GenerateNext", + 34.65809932987622 + ], + [ + "FSharp.Core!Microsoft.FSharp.Primitives.Basics.List.map2", + 34.18549927694585 + ], + [ + "FSharp.Core!Microsoft.FSharp.Primitives.Basics.List.forall", + 27.89040215909472 + ], + [ + "FSharp.Core!Microsoft.FSharp.Collections.SetTreeModule.mk", + 24.955597100999057 + ], + [ + "FSharp.Core!Microsoft.FSharp.Primitives.Basics.List.rev", + 23.69919886070602 + ], + [ + "FSharp.Core!Microsoft.FSharp.Primitives.Basics.List.filterToFreshConsTail", + 22.567697390302328 + ] + ], + "callers": [ + [ + "FSharp.Compiler.Service!Internal.Utilities.TypeHashing+StructuralUtilities+TypeStructure.GetHashCode", + 421.1036214466226 + ], + [ + "FSharp.Compiler.Service!Internal.Utilities.TypeHashing+StructuralUtilities.getTypeStructureOfStrippedType", + 285.3314929863145 + ], + [ + "FSharp.Compiler.Service!FSharp.Compiler.TypeRelations+TTypeCacheKey.Equals", + 263.61745132523174 + ], + [ + "FSharp.Compiler.Service!FSharp.Compiler.CheckIncrementalClasses.MakeCtorForIncrClassConstructionPhase2C", + 152.98410232028937 + ], + [ + "FSharp.Compiler.Service!FSharp.Compiler.TypedTreeOps.stripTyEqnsA", + 134.8714933675392 + ], + [ + "FSharp.Compiler.Service!.$Caches+post@339[FSharp.Compiler.TypeRelations+TTypeCacheKey,System.Boolean].Invoke", + 83.43699641324383 + ], + [ + "FSharp.Compiler.Service!FSharp.Compiler.MethodCalls.BuildFSharpMethodApp", + 74.97760034636258 + ], + [ + "FSharp.Compiler.Service!FSharp.Compiler.TypedTreeOps.bindLocalVals", + 74.46530329191683 + ], + [ + "FSharp.Compiler.Service!FSharp.Compiler.CheckDeclarations+AddAugmentationDeclarations.AddGenericEqualityBindings$cont@884", + 74.22199729003887 + ], + [ + "FSharp.Compiler.Service!FSharp.Compiler.TypeHierarchy.loop@233-46", + 73.37619140961851 + ], + [ + "FSharp.Compiler.Service!FSharp.Compiler.TypeHierarchy.GetImmediateInterfacesOfMetadataType", + 59.12099778760785 + ], + [ + "FSharp.Compiler.Service!FSharp.Compiler.MethodCalls.AdjustCallerArgs", + 54.395699605772734 + ], + [ + "FSharp.Compiler.Service!FSharp.Compiler.MethodCalls+CalledMeth`1[System.__Canon]..ctor", + 52.76969915767904 + ], + [ + "FSharp.Compiler.Service!FSharp.Compiler.TypedTreeOps+TyconRefMultiMap`1[System.__Canon].Add", + 52.33259791507362 + ], + [ + "FSharp.Compiler.Service!FSharp.Compiler.TypedTreeOps.GetTopTauTypeInFSharpForm", + 51.86739932809178 + ], + [ + "FSharp.Compiler.Service!FSharp.Compiler.TypedTreeOps.applyForallTy", + 51.43339851814335 + ], + [ + "FSharp.Compiler.Service!FSharp.Compiler.TypedTreeBasics.mkNestedNonLocalEntityRef", + 43.55512265962625 + ], + [ + "FSharp.Compiler.Service!FSharp.Compiler.MethodCalls+argSetInfos@578-1[System.__Canon].Invoke", + 43.36459915781188 + ], + [ + "FSharp.Compiler.Service!FSharp.Compiler.NameResolution.AddTyconByAccessNames", + 38.678199606272756 + ], + [ + "FSharp.Compiler.Service!FSharp.Compiler.TypedTreeOps.destForallTy", + 38.15679949852938 + ] + ], + "allocations": { + "trace": "temp/core-profile/js-2.nettrace", + "ticks": 77857, + "missingStacks": 1, + "eventsLost": 0, + "totalSampledBytes": 8335642456, + "checkingSampledBytes": 4092520992, + "selected": [ + { + "name": "TypeToken boxing under array equality", + "estimatedMB": 100.06058502197266 + }, + { + "name": "TypeToken boxing under array hashing", + "estimatedMB": 45.88446807861328 + }, + { + "name": "Map nodes under entity-name table construction", + "estimatedMB": 4.06427001953125 + } + ], + "types": [ + { + "name": "emitTType@443", + "estimatedMB": 278.43419647216797 + }, + { + "name": "TType_app", + "estimatedMB": 202.35404205322266 + }, + { + "name": "TypeToken", + "estimatedMB": 145.94505310058594 + }, + { + "name": "Microsoft.FSharp.Collections.FSharpList`1[FSharp.Compiler.TypedTree+TType]", + "estimatedMB": 115.58814239501953 + }, + { + "name": "System.Tuple`6[System.Int32,System.Int32,System.Int32,FSharp.Compiler.AbstractIL.ILBinaryReader+TaggedIndex`1[FSharp.Compiler.AbstractIL.BinaryConstants+TypeDefOrRefTag],System.Int32,System.Int32]", + "estimatedMB": 103.71931457519531 + }, + { + "name": "emitNullness@418", + "estimatedMB": 100.49309539794922 + }, + { + "name": "System.Tuple`5[System.Int32,System.UInt16,System.UInt16,FSharp.Compiler.AbstractIL.ILBinaryReader+TaggedIndex`1[FSharp.Compiler.AbstractIL.BinaryConstants+TypeOrMethodDefTag],System.Int32]", + "estimatedMB": 98.86675262451172 + }, + { + "name": "TypeToken[]", + "estimatedMB": 60.84674835205078 + }, + { + "name": "ILTypeRef", + "estimatedMB": 59.223060607910156 + }, + { + "name": "Truncate@1081[Internal.Utilities.TypeHashing+StructuralUtilities+TypeToken]", + "estimatedMB": 46.733673095703125 + } + ] + } + } +] \ No newline at end of file diff --git a/notes/benchmarks/core-runtime/profile.ps1 b/notes/benchmarks/core-runtime/profile.ps1 new file mode 100644 index 0000000000..65020a5a5c --- /dev/null +++ b/notes/benchmarks/core-runtime/profile.ps1 @@ -0,0 +1,36 @@ +param([ValidateSet('binding', 'js')][string]$Project = 'binding', [int]$Round = 1) +$ErrorActionPreference = 'Stop' +$root = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '../../..')) +Set-Location $root +$dest = Join-Path $root 'temp/core-profile' +New-Item -ItemType Directory -Force $dest | Out-Null +$fixture = if ($Project -eq 'binding') { 'temp/binding-probe/Binding.fsproj' } else { 'tests/Js/Main/Fable.Tests.fsproj' } +$name = "$Project-$Round" +$previousActivities = $env:FICKL_FCS_ACTIVITIES +$env:FICKL_FCS_ACTIVITIES = "$dest/$name-activities.csv" +try { + $info = New-Object Diagnostics.ProcessStartInfo + $info.FileName = 'dotnet' + $info.Arguments = "`"$root/temp/core-aligned/baseline/fable.dll`" $fixture --outDir `"$dest/output/$Project`" --noCache --noRestore --noPackageCache --timings --exclude Fable.Core" + $info.UseShellExecute = $false + $info.CreateNoWindow = $true + $info.RedirectStandardOutput = $true + $info.RedirectStandardError = $true + $proc = [Diagnostics.Process]::Start($info) + $stdout = $proc.StandardOutput.ReadToEndAsync() + $stderr = $proc.StandardError.ReadToEndAsync() + & rtk proxy "$root/temp/binding-probe/tools/dotnet-trace.exe" collect --process-id $proc.Id --profile dotnet-sampled-thread-time,gc-verbose --format Speedscope --output "$dest/$name.nettrace" + $traceCode = $LASTEXITCODE + $proc.WaitForExit() + ($stdout.Result + $stderr.Result) | Set-Content "$dest/$name.log" + if ($traceCode -ne 0 -or $proc.ExitCode -ne 0) { throw "Profile failed: $name" } + $sha = [Security.Cryptography.SHA256]::Create() + try { + Get-ChildItem "$dest/output/$Project" -Recurse -Filter '*.js' | Sort-Object FullName | ForEach-Object { + [pscustomobject]@{ Path = $_.FullName.Substring(("$dest/output/$Project").Length); SHA256 = [BitConverter]::ToString($sha.ComputeHash([IO.File]::ReadAllBytes($_.FullName))) } + } | Export-Csv "$dest/$name-output.csv" -NoTypeInformation + } finally { $sha.Dispose() } + $stdout.Result +} finally { + $env:FICKL_FCS_ACTIVITIES = $previousActivities +} diff --git a/notes/benchmarks/core-runtime/test-stack-analysis.py b/notes/benchmarks/core-runtime/test-stack-analysis.py new file mode 100644 index 0000000000..6fb13c35be --- /dev/null +++ b/notes/benchmarks/core-runtime/test-stack-analysis.py @@ -0,0 +1,37 @@ +"""Check that callback time and unmanaged waits are not charged to Core.""" + +import json +from pathlib import Path +import subprocess +import tempfile + +names = [ + "FSharp.Compiler.Service!FSharp.Compiler.CheckExpressions.Tc()", + "FSharp.Core!Microsoft.FSharp.Collections.MapTreeModule.mk()", + "FSharp.Compiler.Service!FSharp.Compiler.CheckExpressions.Callback()", + "System.Private.CoreLib!System.String.Compare()", + "CPU_TIME", + "UNMANAGED_CODE_TIME", + "FSharp.Compiler.Service!FSharp.Compiler.Other.Work()", +] +events = [] +time = 0 +for stack, duration in [([0, 1, 4], 10), ([0, 1, 2, 4], 20), ([0, 1, 5], 30), ([0, 1, 3, 4], 40), ([6, 1, 4], 50)]: + events.extend(dict(type="O", frame=f, at=time) for f in stack) + time += duration + events.extend(dict(type="C", frame=f, at=time) for f in reversed(stack)) +fixture = dict( + shared=dict(frames=[dict(name=n) for n in names]), profiles=[dict(startValue=0, endValue=time, events=events)] +) +with tempfile.TemporaryDirectory() as directory: + path = Path(directory) / "test.json" + path.write_text(json.dumps(fixture)) + output = subprocess.check_output( + ["rtk", "proxy", "python", str(Path(__file__).with_name("analyze-stacks.py")), str(path)], text=True + ) + result = json.loads(output)[0] +assert result["sampled_check_ms"] == 70, result +assert result["core_inclusive_ms"] == 50, result +assert result["leaf"][0][1] == 10, result +assert result["inclusive"][0][1] == 50, result +print("PASS: CPU-only accounting, compiler callback exclusion, Core self time, checking scope") diff --git a/notes/benchmarks/fickl-0.5.3-manifest.json b/notes/benchmarks/fickl-0.5.3-manifest.json new file mode 100644 index 0000000000..844049ebb9 --- /dev/null +++ b/notes/benchmarks/fickl-0.5.3-manifest.json @@ -0,0 +1,25 @@ +{ + "platform": "Windows-11-10.0.26200-SP0", + "dotnet_sdk": "10.0.113", + "compiler_commit": "4194dcca6c37895bbb431ace58690a5ac4a0711e", + "candidate": "Fickl 0.5.3, published 2026-10-22 from the validated packages below", + "packages": { + "Fickl.0.5.3.nupkg": "d5e114261d198c64099b80a927e6d8b9df756f75b263f9b6012c696b0cc7957e", + "Fickl.any.0.5.3.nupkg": "b87cb5f5665b7c8ff0f7179ba92e3c2df9616f8fcbbd40fa0cdb267708c42b80", + "Fickl.AST.0.5.3.nupkg": "4d0de0ff8bc049bd475e39427bae9e499a01bf415fa00b361139ddf3c78ac903", + "Fickl.Compiler.0.5.3.nupkg": "ad0aa763a54e293202b18eb44e6967efdf611fe981c7b5e6dad2a48f081e1e51", + "Fickl.Core.0.5.3.nupkg": "e203a58a719980245bd72b633c477fd9085b14eb6e6817835f622ed53a64eb54", + "Fickl.linux-arm64.0.5.3.nupkg": "1b4461862f333a0dd85f6aabe063c3288f2ff869b08878b16c55dc99f538a0b0", + "Fickl.linux-musl-x64.0.5.3.nupkg": "9e08a989f5bde829963ced77e7be0b91a4e21919768e2229ba1ea291fa2c1475", + "Fickl.linux-x64.0.5.3.nupkg": "ec24d3a0d7f1f5f9c6aaa70b2cb509268f4a2aabcb47079b7cb32ef54c40ea3e", + "Fickl.osx-arm64.0.5.3.nupkg": "ad639cbfb616adedeff39fe98d4c95ec799c7c431ca710f39fd3cbf7203e80e7", + "Fickl.osx-x64.0.5.3.nupkg": "f3b9df7e5beab695645af06e3eb473e4a29fa12d9a57e84908df59e6d2977b97", + "Fickl.PublishUtils.0.5.3.nupkg": "c825633aa109f075ea3e809a5611761dbf19654e1149a5ed979b19a357a9a613", + "Fickl.win-arm64.0.5.3.nupkg": "bea7068c889b1c87c3929352b3d46e6290933480e387eb8c41eba7ee797eb33d", + "Fickl.win-x64.0.5.3.nupkg": "2fd56b914b9683933565b8f5f720e22ec39283ce05228c8c18f23e26907a6561" + }, + "resumed_workloads": [ + "jssuite" + ], + "resume_reason": "Correct Fickl.Core project identity; packaged tool files byte-identical after repack" +} diff --git a/notes/benchmarks/fickl-0.5.3.csv b/notes/benchmarks/fickl-0.5.3.csv new file mode 100644 index 0000000000..3d416e9fb8 --- /dev/null +++ b/notes/benchmarks/fickl-0.5.3.csv @@ -0,0 +1,151 @@ +project,configuration,run,wall_ms,parsed_ms,compile_ms,exit_code +py,fable,-1,11773,2244,8774,0 +py,fableq,-1,10291,517,9003,0 +py,nopkg,-1,8954,352,7238,0 +py,hit,-1,15973,357,6636,0 +py,miss,0,16413,402,6812,0 +py,fable,1,11235,1818,8648,0 +py,hit,1,1799,367,714,0 +py,fableq,1,9893,470,8692,0 +py,nopkg,1,7482,368,6708,0 +py,nopkg,2,7346,346,6606,0 +py,fableq,2,10074,479,8832,0 +py,hit,2,1812,345,711,0 +py,fable,2,11317,1815,8749,0 +py,fable,3,11378,1708,8924,0 +py,hit,3,1838,369,716,0 +py,fableq,3,9862,464,8646,0 +py,nopkg,3,7459,352,6685,0 +py,nopkg,4,7433,370,6659,0 +py,fableq,4,9931,464,8718,0 +py,hit,4,1799,351,713,0 +py,fable,4,11103,1771,8596,0 +py,fable,5,11202,1804,8648,0 +py,hit,5,1766,335,686,0 +py,fableq,5,9703,475,8469,0 +py,nopkg,5,7325,345,6570,0 +beam,fable,-1,10174,1715,7679,0 +beam,fableq,-1,8945,477,7696,0 +beam,nopkg,-1,7031,341,6266,0 +beam,hit,-1,15855,358,6348,0 +beam,miss,0,15873,338,6110,0 +beam,fable,1,10316,1764,7815,0 +beam,hit,1,1519,338,595,0 +beam,fableq,1,9166,500,7908,0 +beam,nopkg,1,7092,338,6358,0 +beam,nopkg,2,7068,328,6346,0 +beam,fableq,2,9303,463,8099,0 +beam,hit,2,1522,333,604,0 +beam,fable,2,10713,1828,8121,0 +beam,fable,3,10418,1767,7906,0 +beam,hit,3,1507,321,598,0 +beam,fableq,3,8996,499,7715,0 +beam,nopkg,3,6887,324,6159,0 +beam,nopkg,4,7348,330,6624,0 +beam,fableq,4,8959,456,7769,0 +beam,hit,4,1540,336,607,0 +beam,fable,4,10618,1797,8079,0 +beam,fable,5,10584,1750,8070,0 +beam,hit,5,1510,331,600,0 +beam,fableq,5,9199,509,7938,0 +beam,nopkg,5,7294,336,6556,0 +sutil,fable,-1,9173,2635,5746,0 +sutil,fableq,-1,7217,661,5751,0 +sutil,nopkg,-1,4102,448,3208,0 +sutil,hit,-1,10300,425,3139,0 +sutil,miss,0,10393,466,3106,0 +sutil,fable,1,9212,2684,5760,0 +sutil,hit,1,2389,433,1312,0 +sutil,fableq,1,7294,653,5879,0 +sutil,nopkg,1,3992,440,3141,0 +sutil,nopkg,2,4111,464,3205,0 +sutil,fableq,2,7490,697,6005,0 +sutil,hit,2,2418,417,1341,0 +sutil,fable,2,9240,2650,5823,0 +sutil,fable,3,9085,2591,5701,0 +sutil,hit,3,2369,424,1292,0 +sutil,fableq,3,7233,653,5800,0 +sutil,nopkg,3,4095,406,3265,0 +sutil,nopkg,4,3990,415,3132,0 +sutil,fableq,4,7238,691,5752,0 +sutil,hit,4,2385,431,1301,0 +sutil,fable,4,9191,2608,5800,0 +sutil,fable,5,9202,2631,5796,0 +sutil,hit,5,2409,411,1339,0 +sutil,fableq,5,7276,694,5777,0 +sutil,nopkg,5,3991,424,3136,0 +fantomas,fable,-1,7472,1820,4687,0 +fantomas,fableq,-1,6192,536,4687,0 +fantomas,nopkg,-1,2725,402,1728,0 +fantomas,hit,-1,6229,403,1715,0 +fantomas,miss,0,6355,417,1725,0 +fantomas,fable,1,7509,1847,4686,0 +fantomas,hit,1,1919,397,665,0 +fantomas,fableq,1,6283,535,4756,0 +fantomas,nopkg,1,2691,388,1704,0 +fantomas,nopkg,2,2638,387,1718,0 +fantomas,fableq,2,6198,529,4678,0 +fantomas,hit,2,1872,398,624,0 +fantomas,fable,2,7561,1851,4761,0 +fantomas,fable,3,7632,1859,4805,0 +fantomas,hit,3,1924,422,670,0 +fantomas,fableq,3,6298,519,4799,0 +fantomas,nopkg,3,2798,403,1779,0 +fantomas,nopkg,4,2612,398,1670,0 +fantomas,fableq,4,6432,533,4901,0 +fantomas,hit,4,1868,388,667,0 +fantomas,fable,4,7619,1857,4800,0 +fantomas,fable,5,7688,1929,4800,0 +fantomas,hit,5,1925,404,654,0 +fantomas,fableq,5,6285,530,4786,0 +fantomas,nopkg,5,2684,414,1706,0 +ionide,fable,-1,12357,7947,3639,0 +ionide,fableq,-1,5600,1191,3641,0 +ionide,nopkg,-1,2946,887,1636,0 +ionide,hit,-1,4979,879,1631,0 +ionide,miss,0,4910,907,1600,0 +ionide,fable,1,11685,7384,3552,0 +ionide,hit,1,2762,879,1317,0 +ionide,fableq,1,5587,1204,3637,0 +ionide,nopkg,1,2907,880,1643,0 +ionide,nopkg,2,2884,913,1589,0 +ionide,fableq,2,5561,1199,3617,0 +ionide,hit,2,2787,904,1335,0 +ionide,fable,2,12216,7785,3663,0 +ionide,fable,3,12519,8097,3656,0 +ionide,hit,3,2872,900,1378,0 +ionide,fableq,3,5618,1234,3625,0 +ionide,nopkg,3,2935,897,1621,0 +ionide,nopkg,4,2905,924,1586,0 +ionide,fableq,4,5716,1236,3683,0 +ionide,hit,4,2835,907,1327,0 +ionide,fable,4,12391,8049,3585,0 +ionide,fable,5,11961,7647,3553,0 +ionide,hit,5,2855,893,1389,0 +ionide,fableq,5,5602,1238,3593,0 +ionide,nopkg,5,2936,883,1651,0 +jssuite,fable,-1,32533,25255,6342,0 +jssuite,fableq,-1,9033,1656,6380,0 +jssuite,nopkg,-1,3897,1299,2081,0 +jssuite,hit,-1,5349,1291,2179,0 +jssuite,miss,0,5133,1287,2050,0 +jssuite,fable,1,28737,21587,6231,0 +jssuite,hit,1,3942,1338,2007,0 +jssuite,fableq,1,8855,1630,6316,0 +jssuite,nopkg,1,3895,1370,2016,0 +jssuite,nopkg,2,4071,1325,2216,0 +jssuite,fableq,2,8815,1697,6220,0 +jssuite,hit,2,3905,1340,1979,0 +jssuite,fable,2,28217,21032,6286,0 +jssuite,fable,3,28306,21092,6303,0 +jssuite,hit,3,3914,1345,2006,0 +jssuite,fableq,3,8922,1669,6305,0 +jssuite,nopkg,3,4158,1271,2326,0 +jssuite,nopkg,4,4112,1327,2230,0 +jssuite,fableq,4,8970,1636,6374,0 +jssuite,hit,4,3939,1324,2059,0 +jssuite,fable,4,27792,20535,6371,0 +jssuite,fable,5,28284,20907,6473,0 +jssuite,hit,5,4020,1346,2085,0 +jssuite,fableq,5,8872,1647,6322,0 +jssuite,nopkg,5,3929,1344,2078,0 diff --git a/notes/benchmarks/fickl-0.5.3.png b/notes/benchmarks/fickl-0.5.3.png new file mode 100644 index 0000000000..cea4798cd7 Binary files /dev/null and b/notes/benchmarks/fickl-0.5.3.png differ diff --git a/notes/benchmarks/plot-0.5.3.py b/notes/benchmarks/plot-0.5.3.py new file mode 100644 index 0000000000..74883a3ce0 --- /dev/null +++ b/notes/benchmarks/plot-0.5.3.py @@ -0,0 +1,102 @@ +import csv +from pathlib import Path +from statistics import median +import matplotlib + +matplotlib.use("Agg") +import matplotlib.pyplot as plt +from matplotlib.patches import Patch + +root = Path(__file__).resolve().parents[2] +rows = list(csv.DictReader((root / "temp/release-0.5.3/benchmark/results.csv").open())) +names = [ + ("sutil", "Sutil demo app"), + ("fantomas", "Fantomas online tools"), + ("ionide", "Ionide for VS Code"), + ("jssuite", "Fable JS test suite"), + ("py", "Python package workload"), + ("beam", "BEAM package workload"), +] +configs = ["fable", "fableq", "nopkg", "hit"] +labels = [ + "Fable 5.22", + "Fable 5.22 · telemetry opted out", + "Fickl 0.5.3 · package cache off", + "Fickl 0.5.3 · package cache hit", +] +colors = ["#2876cf", "#99bde8", "#ea713e", "#13a477"] +values = {} +for name, _ in names: + for cfg in configs: + selected = [r for r in rows if r["project"] == name and r["configuration"] == cfg and int(r["run"]) > 0] + assert len(selected) == 5 and all(int(r["exit_code"]) == 0 for r in selected), (name, cfg) + values[name, cfg] = median(float(r["wall_ms"]) / 1000 for r in selected) +plt.rcParams.update( + {"font.family": "DejaVu Sans", "font.size": 11, "text.color": "#17232b", "axes.labelcolor": "#50616d"} +) +fig, axes = plt.subplots(6, 1, figsize=(13, 11.5)) +fig.patch.set_facecolor("#fafbf9") +fig.subplots_adjust(top=0.82, bottom=0.16, left=0.245, right=0.88, hspace=0.42) +fig.text(0.035, 0.956, "Fickl 0.5.3 against Fable 5.22", fontsize=27, fontweight="bold") +fig.text( + 0.035, + 0.919, + "End-to-end compilation time · fresh process · median of 5 interleaved runs", + fontsize=12, + color="#50616d", +) +fig.legend( + handles=[Patch(color=c, label=l) for c, l in zip(colors, labels)], + loc="upper left", + bbox_to_anchor=(0.027, 0.893), + ncol=2, + frameon=False, + fontsize=11, +) +limit = max(values.values()) * 1.13 +for ax, (name, label) in zip(axes, names): + ax.set_facecolor("#fafbf9") + for i, cfg in enumerate(configs): + v = values[name, cfg] + ax.barh(i, v, height=0.77, color=colors[i]) + ax.text(v + limit * 0.009, i, f"{v:.1f} s", va="center", fontsize=10) + ax.invert_yaxis() + ax.set_xlim(0, limit) + ax.set_yticks([]) + ax.set_axisbelow(True) + ax.grid(axis="x", color="#dfe5e8", linewidth=0.7) + ax.tick_params(axis="x", length=0, labelsize=9, colors="#657580") + for spine in ax.spines.values(): + spine.set_visible(False) + ax.text(-0.035, 0.5, label, transform=ax.transAxes, ha="right", va="center", fontsize=12, fontweight="bold") + ratio = values[name, "fableq"] / values[name, "hit"] + ax.text(1.04, 0.58, f"{ratio:.1f}×", transform=ax.transAxes, fontsize=19, fontweight="bold", color=colors[3]) + ax.text(1.04, 0.26, "faster*", transform=ax.transAxes, fontsize=9, color="#657580") +axes[-1].set_xlabel("Wall-clock seconds — lower is better", labelpad=8) +fig.text( + 0.035, + 0.083, + "*Cache-hit speedup versus Fable with DOTNET_CLI_TELEMETRY_OPTOUT=1. All runs use --noCache; restores are included.", + fontsize=9, + color="#50616d", +) +fig.text( + 0.035, + 0.062, + "Package-cache hits are warmed; first-use cache construction is excluded. JS suite uses --exclude Fable.Core.", + fontsize=9, + color="#50616d", +) +fig.text( + 0.035, + 0.041, + "Windows x64 · .NET 10 · 22 Oct 2026. Telemetry/network overhead is machine-specific. Glutinum excluded: both compilers fail.", + fontsize=9, + color="#50616d", +) +out = root / "notes/benchmarks" +out.mkdir(exist_ok=True) +fig.savefig(out / "fickl-0.5.3.png", dpi=160, facecolor=fig.get_facecolor()) +(out / "fickl-0.5.3.csv").write_text((root / "temp/release-0.5.3/benchmark/results.csv").read_text()) +for name, _ in names: + print(name, {cfg: values[name, cfg] for cfg in configs}) diff --git a/notes/benchmarks/type-token-arrays/Program.fs b/notes/benchmarks/type-token-arrays/Program.fs new file mode 100644 index 0000000000..0b7f0ea64a --- /dev/null +++ b/notes/benchmarks/type-token-arrays/Program.fs @@ -0,0 +1,188 @@ +open System +open System.Reflection +open Microsoft.FSharp.Reflection + +let flags = BindingFlags.Public ||| BindingFlags.NonPublic +let args = Environment.GetCommandLineArgs() |> Array.skip 1 +let assembly = Assembly.LoadFrom(IO.Path.GetFullPath args[0]) + +let findType (suffix: string) = + assembly.GetTypes() |> Array.find (fun t -> t.FullName.EndsWith suffix) + +let tokenType = findType "+TypeToken" +let structureType = findType "+TypeStructure" +let keyType = findType "+TTypeCacheKey" +let coerceType = findType "+CanCoerce" +let cases t = FSharpType.GetUnionCases(t, flags) +let make c fields = FSharpValue.MakeUnion(c, fields, flags) + +let values t = + if t = typeof then + [| null; box ""; box "x"; box "\u03bb" |] + elif t = typeof then + [| box Int32.MinValue; box -1; box 0; box 1; box Int32.MaxValue |] + elif t = typeof then + [| box Int64.MinValue; box -1L; box 0L; box 1L; box Int64.MaxValue |] + elif t = typeof then + [| box false; box true |] + else + cases t |> Array.map (fun c -> make c [||]) + +let tokens = + cases tokenType + |> Array.collect (fun c -> + let fields = c.GetFields() |> Array.map (fun f -> values f.PropertyType) + + if fields.Length = 0 then + [| make c [||] |] + else + [| for i in 0..4 -> make c (fields |> Array.map (fun v -> v[i % v.Length])) |] + ) + +let arrayOf (items: obj[]) = + let result = Array.CreateInstance(tokenType, items.Length) + items |> Array.iteri (fun i value -> result.SetValue(value, i)) + box result + +let arrays = ResizeArray() +arrays.Add null + +for token in tokens do + arrays.Add(arrayOf [| token |]) + +for length in [ 0; 1; 2; 17; 18; 19; 20; 21; 254; 255; 256 ] do + let items = Array.init length (fun i -> tokens[i % tokens.Length]) + arrays.Add(arrayOf items) + arrays.Add(arrayOf items) // separately allocated equal array + + for index in [ 0; 18; 19; length - 1 ] |> List.distinct do + if index >= 0 && index < length then + let changed = Array.copy items + changed[index] <- tokens[(index + 1) % tokens.Length] + arrays.Add(arrayOf changed) + +let structures = + cases structureType + |> Array.collect (fun c -> + if c.GetFields().Length = 0 then + [| make c [||] |] + else + arrays |> Seq.map (fun a -> make c [| a |]) |> Seq.toArray + ) + +let coercions = cases coerceType |> Array.map (fun c -> make c [||]) +let keyCase = (cases keyType)[0] + +let keys = + [| + for i in 0 .. structures.Length - 1 do + for coerce in coercions do + yield make keyCase [| structures[i]; structures[(i * 7) % structures.Length]; coerce |] + yield make keyCase [| structures[i]; structures[i]; coerce |] + |] + +let oracle = HashIdentity.Structural +let candidate = args |> Array.contains "--candidate" + +let equal, hash = + if candidate then + let moduleType = assembly.GetType("FSharp.Compiler.TypeRelations", true) + + let getter = + moduleType.GetMethod("get_typeCacheKeyComparer", flags ||| BindingFlags.Static) + + if isNull getter then + failwith "Missing candidate typeCacheKeyComparer" + + let comparer = getter.Invoke(null, [||]) + + let iface = + typedefof>.MakeGenericType(keyType) + + let eqMethod = iface.GetMethod "Equals" + let hashMethod = iface.GetMethod "GetHashCode" + + (fun x y -> eqMethod.Invoke(comparer, [| x; y |]) :?> bool), + (fun x -> hashMethod.Invoke(comparer, [| x |]) :?> int) + else + (fun x y -> oracle.Equals(x, y)), oracle.GetHashCode + +let mutable checks = 0 +// A default key has null TypeStructure fields. Preserve the baseline exception, +// rather than treating this unreachable-in-normal-use value as a valid key. +let hashOutcome f value = + try + Choice1Of2(f value) + with ex -> + let actual = + match ex with + | :? TargetInvocationException as e -> e.InnerException + | _ -> ex + + Choice2Of2(actual.GetType().FullName) + +let defaultKey = Activator.CreateInstance keyType + +if hashOutcome hash defaultKey <> hashOutcome oracle.GetHashCode defaultKey then + failwith "Default-key hash exception changed" + +for key in keys do + let actual = + hash key + ^^^ (if Array.contains "--mutate" args then + 1 + else + 0) + + if oracle.GetHashCode key <> actual then + failwithf "Hash mismatch at key %d" checks + + if not (equal key key) then + failwith "Self equality failed" + + checks <- checks + 2 +// Include separately allocated equal structures, both case tags and coercion flags. +for i in 0 .. keys.Length - 1 do + let c, fields = FSharpValue.GetUnionFields(keys[i], keyType, flags) + let clone = make c fields + + if not (equal keys[i] clone) then + failwith "Clone equality failed" + +let random = Random 1729 + +for _ in 1..20000 do + let x, y = keys[random.Next keys.Length], keys[random.Next keys.Length] + + if equal x y <> oracle.Equals(x, y) then + failwith "Equality mismatch" + + checks <- checks + 1 +// Exhaustive pairwise coverage of all token cases and fields in single-element arrays. +let singleKeys = + [| + for token in tokens -> + let structure = make ((cases structureType)[0]) [| arrayOf [| token |] |] + make keyCase [| structure; structure; coercions[0] |] + |] + +for x in singleKeys do + for y in singleKeys do + if equal x y <> oracle.Equals(x, y) then + failwith "Token equality mismatch" + + checks <- checks + 1 +// Exhaustively compare the array boundary cases, including equal distinct arrays +// and differences past the hash prefix that must still make equality false. +let structureKeys = + structures + |> Array.map (fun s -> make keyCase [| s; structures[0]; coercions[0] |]) + +for x in structureKeys do + for y in structureKeys do + if equal x y <> oracle.Equals(x, y) then + failwith "Array boundary equality mismatch" + + checks <- checks + 1 + +printfn "PASS: %d checks, %d keys, %d token values; exact hashes and equality match" checks keys.Length tokens.Length diff --git a/notes/benchmarks/type-token-arrays/TokenTests.fsproj b/notes/benchmarks/type-token-arrays/TokenTests.fsproj new file mode 100644 index 0000000000..778df0d0d8 --- /dev/null +++ b/notes/benchmarks/type-token-arrays/TokenTests.fsproj @@ -0,0 +1,7 @@ + + Exenet10.0true + + + + + diff --git a/notes/benchmarks/type-token-arrays/direct-comparer-experiment.patch b/notes/benchmarks/type-token-arrays/direct-comparer-experiment.patch new file mode 100644 index 0000000000..d2eb6dc30a --- /dev/null +++ b/notes/benchmarks/type-token-arrays/direct-comparer-experiment.patch @@ -0,0 +1,71 @@ +diff --git a/src/Compiler/Checking/TypeRelations.fs b/src/Compiler/Checking/TypeRelations.fs +index 4122f1407..15ba9118a 100644 +--- a/src/Compiler/Checking/TypeRelations.fs ++++ b/src/Compiler/Checking/TypeRelations.fs +@@ -38,12 +38,64 @@ type TTypeCacheKey = + (tryGetTypeStructure ty1, tryGetTypeStructure ty2) + ||> ValueOption.map2(fun t1 t2 -> TTypeCacheKey(t1, t2, canCoerce)) + ++// This cache uses only these internal token vectors, not arbitrary arrays or ++// user-supplied comparers. Keep the ordinary F# structural equality/hash contract ++// while avoiding Array.GetValue boxing and boxing of the cache-key struct. ++let private typeCacheKeyComparer = ++ let tokenComparer = HashIdentity.Structural ++ let equalTokens (xs: TypeToken[]) (ys: TypeToken[]) = ++ if isNull xs then isNull ys ++ elif isNull ys then false ++ else ++ let mutable equal = xs.Length = ys.Length ++ let mutable i = 0 ++ while equal && i < xs.Length do ++ equal <- tokenComparer.Equals(xs[i], ys[i]) ++ i <- i + 1 ++ equal ++ let equalStructure x y = ++ if obj.ReferenceEquals(x, null) then obj.ReferenceEquals(y, null) ++ elif obj.ReferenceEquals(y, null) then false ++ else ++ match x, y with ++ | Stable xs, Stable ys | Unstable xs, Unstable ys -> equalTokens xs ys ++ | PossiblyInfinite, PossiblyInfinite -> true ++ | _ -> false ++ let hashTokens (tokens: TypeToken[]) = ++ if isNull tokens then 0 ++ else ++ // Core hashes indices min(length-1,18) down to zero. Equality above ++ // still checks the entire array, including elements after this prefix. ++ let mutable i = min (tokens.Length - 1) 18 ++ let mutable acc = 0 ++ while i >= 0 do ++ acc <- (acc <<< 1) + tokenComparer.GetHashCode(tokens[i]) + 631 * i ++ i <- i - 1 ++ acc ++ // Match generated union hashing: case tag seed, fields in reverse order. ++ // See AugmentWithHashCompare.mkCombineHashGenerators and mkUnionHashWithComparer. ++ let combine acc value = -1640531527 + value + (acc <<< 6) + (acc >>> 2) ++ let hashStructure structure = ++ if obj.ReferenceEquals(structure, null) then 0 ++ else ++ match structure with ++ | Stable tokens -> combine 0 (hashTokens tokens) ++ | Unstable tokens -> combine 1 (hashTokens tokens) ++ | PossiblyInfinite -> 2 ++ { new System.Collections.Generic.IEqualityComparer with ++ member _.Equals(TTypeCacheKey(x1, x2, xc), TTypeCacheKey(y1, y2, yc)) = ++ equalStructure x1 y1 && equalStructure x2 y2 && xc = yc ++ member _.GetHashCode(TTypeCacheKey(x1, x2, canCoerce)) = ++ let acc = combine 0 (hash canCoerce) ++ let acc = combine acc (hashStructure x2) ++ combine acc (hashStructure x1) } ++ + let getTypeSubsumptionCache = + let factory (g: TcGlobals) = + let options = + match g.compilationMode with +- | CompilationMode.OneOff -> Caches.CacheOptions.getDefault HashIdentity.Structural |> Caches.CacheOptions.withNoEviction +- | _ -> { Caches.CacheOptions.getDefault HashIdentity.Structural with TotalCapacity = 65536; HeadroomPercentage = 75 } ++ | CompilationMode.OneOff -> Caches.CacheOptions.getDefault typeCacheKeyComparer |> Caches.CacheOptions.withNoEviction ++ | _ -> { Caches.CacheOptions.getDefault typeCacheKeyComparer with TotalCapacity = 65536; HeadroomPercentage = 75 } + new Caches.Cache(options, "typeSubsumptionCache") + Extras.WeakMap.getOrCreate factory + diff --git a/notes/benchmarks/type-token-arrays/wrapper-comparison.csv b/notes/benchmarks/type-token-arrays/wrapper-comparison.csv new file mode 100644 index 0000000000..ec7f1c96cb --- /dev/null +++ b/notes/benchmarks/type-token-arrays/wrapper-comparison.csv @@ -0,0 +1,25 @@ +"Project","Variant","Round","WallMs","CheckMs","CompileMs","AllocatedMB","IdenticalFiles" +"binding","baseline","0","4786","1924","2071","1219","67" +"binding","candidate","0","7558","4998","5520","1219","67" +"binding","candidate","1","4650","1925","2078","1218","67" +"binding","baseline","1","4347","1874","2060","1219","67" +"binding","baseline","2","4584","1966","2131","1220","67" +"binding","candidate","2","4447","1998","2164","1219","67" +"binding","candidate","3","6358","2536","2736","1218","67" +"binding","baseline","3","13096","8318","8595","1219","67" +"binding","baseline","4","15993","9765","10036","1218","67" +"binding","candidate","4","7709","3609","3839","1217","67" +"binding","candidate","5","10661","5905","6211","1218","67" +"binding","baseline","5","7352","3368","3632","1219","67" +"js","baseline","0","7584","3207","3625","7788","141" +"js","candidate","0","6026","2470","2882","7728","141" +"js","candidate","1","6231","2766","3133","7732","141" +"js","baseline","1","5652","2532","2988","7806","141" +"js","baseline","2","7366","3067","3534","7800","141" +"js","candidate","2","7362","3365","4052","7721","141" +"js","candidate","3","6089","2707","3124","7724","141" +"js","baseline","3","10880","5477","6284","7783","141" +"js","baseline","4","28603","5189","6014","7801","141" +"js","candidate","4","26266","8804","9557","7688","141" +"js","candidate","5","17593","5214","7005","7714","141" +"js","baseline","5","18219","5637","6746","7787","141" diff --git a/notes/benchmarks/type-token-arrays/wrapper-experiment.patch b/notes/benchmarks/type-token-arrays/wrapper-experiment.patch new file mode 100644 index 0000000000..d827d11ff6 --- /dev/null +++ b/notes/benchmarks/type-token-arrays/wrapper-experiment.patch @@ -0,0 +1,60 @@ +diff --git a/src/Compiler/Checking/TypeRelations.fs b/src/Compiler/Checking/TypeRelations.fs +index 4122f1407..ca361e564 100644 +--- a/src/Compiler/Checking/TypeRelations.fs ++++ b/src/Compiler/Checking/TypeRelations.fs +@@ -38,12 +38,53 @@ type TTypeCacheKey = + (tryGetTypeStructure ty1, tryGetTypeStructure ty2) + ||> ValueOption.map2(fun t1 t2 -> TTypeCacheKey(t1, t2, canCoerce)) + ++// Keep the structural cache-key contract while avoiding Array.GetValue boxing for ++// the TypeToken vectors. This comparer is private to this cache; arbitrary arrays ++// and user-supplied comparers continue to use the ordinary Core implementation. ++let private typeTokenArrayComparer = ++ let tokenComparer = HashIdentity.Structural ++ { new System.Collections.IEqualityComparer with ++ member this.Equals(x, y) = ++ match x, y with ++ | (:? (TypeToken[]) as xs), (:? (TypeToken[]) as ys) -> ++ let mutable equal = xs.Length = ys.Length ++ let mutable i = 0 ++ while equal && i < xs.Length do ++ equal <- tokenComparer.Equals(xs[i], ys[i]) ++ i <- i + 1 ++ equal ++ | null, _ -> isNull y ++ | _, null -> false ++ | (:? System.Collections.IStructuralEquatable as value), _ -> value.Equals(y, this) ++ | _ -> LanguagePrimitives.GenericEqualityComparer.Equals(x, y) ++ member this.GetHashCode(x) = ++ match x with ++ | :? (TypeToken[]) as tokens -> ++ // Core's one-dimensional array hash visits indices min(length-1,18) ++ // down to zero, even for arrays longer than the hashed prefix. ++ let mutable i = min (tokens.Length - 1) 18 ++ let mutable acc = 0 ++ while i >= 0 do ++ acc <- (acc <<< 1) + tokenComparer.GetHashCode(tokens[i]) + 631 * i ++ i <- i - 1 ++ acc ++ | null -> 0 ++ | :? System.Collections.IStructuralEquatable as value -> value.GetHashCode(this) ++ | _ -> LanguagePrimitives.GenericEqualityComparer.GetHashCode(x) } ++ ++let private typeCacheKeyComparer = ++ { new System.Collections.Generic.IEqualityComparer with ++ member _.Equals(x, y) = ++ (x :> System.Collections.IStructuralEquatable).Equals(y, typeTokenArrayComparer) ++ member _.GetHashCode(x) = ++ (x :> System.Collections.IStructuralEquatable).GetHashCode(typeTokenArrayComparer) } ++ + let getTypeSubsumptionCache = + let factory (g: TcGlobals) = + let options = + match g.compilationMode with +- | CompilationMode.OneOff -> Caches.CacheOptions.getDefault HashIdentity.Structural |> Caches.CacheOptions.withNoEviction +- | _ -> { Caches.CacheOptions.getDefault HashIdentity.Structural with TotalCapacity = 65536; HeadroomPercentage = 75 } ++ | CompilationMode.OneOff -> Caches.CacheOptions.getDefault typeCacheKeyComparer |> Caches.CacheOptions.withNoEviction ++ | _ -> { Caches.CacheOptions.getDefault typeCacheKeyComparer with TotalCapacity = 65536; HeadroomPercentage = 75 } + new Caches.Cache(options, "typeSubsumptionCache") + Extras.WeakMap.getOrCreate factory + diff --git a/notes/perf-baseline.md b/notes/perf-baseline.md new file mode 100644 index 0000000000..e7036265c8 --- /dev/null +++ b/notes/perf-baseline.md @@ -0,0 +1,244 @@ +# Performance baseline + +Measured with `dotnet run --project src/Fickl.Build -- timings` (the `--timings` report), Release compiler, cold process, `--noCache`. + +## 2026-10-20: tests/Js/Main → JavaScript (76 source files), Windows 11, .NET 10 + +| Phase | Default (parallel type check) | `--no-parallel-type-check` | +| --- | --- | --- | +| Project cracking (wall) | 2.9–5.5 s | 3.0 s | +| FCS parse + type check (wall) | 8.1–9.6 s | 7.4 s | +| Fable per-file work (summed across threads) | 8.0–9.6 s over 75 files | 30.0 s over 74 files | +| Compilation total (wall, excludes cracking) | 8.2–9.8 s | 9.1 s | +| Allocated | ~16 GB | ~16 GB | +| GC gen0/gen1/gen2 | ~105/50/9 | 138/37/10 | + +Ranges span the runs taken while building the tool; cracking varies most (MSBuild evaluation). + +### Reading it + +- **FCS is the critical path.** With the default parallel type check, Fable compiles each file as soon as FCS + hands it over, and the compilation finishes ~0.2 s after the type check does. Fable's own work hides + almost entirely behind FCS. +- **Fable work parallelises well.** With the type check run first, Fable's 30 s of summed per-file work completes + in ~1.8 s of wall time. (Per-file times inflate under that contention, which is why the sum is higher than + in the default mode.) +- **Allocation is very high**: ~16 GB for 76 files, with 8–10 gen2 collections. Most of it is expected to be + FCS; a dotTrace allocation snapshot should confirm before acting on it. +- **Project cracking** (MSBuild design-time evaluation) is the second-largest cost and is outside FCS. + +## 2026-10-20: same benchmark, net10-only FCS (fickle-fsharp `da62eba`) + +FCS rebuilt for net10.0 only (`dotnet run --project src/Fickl.Build -- fcs-fork`), with no source changes. +Default mode, 3 runs: + +| Phase | netstandard2.0 FCS (above) | net10 FCS | +| --- | --- | --- | +| Project cracking (wall) | 2.9–5.5 s | 3.6–4.0 s | +| FCS parse + type check (wall) | 8.1–9.6 s | 8.4–10.0 s | +| Fable per-file work (summed) | 8.0–9.6 s | 7.8–8.7 s | +| Compilation total (wall) | 8.2–9.8 s | 8.6–10.1 s | +| Allocated | ~16 GB | ~11 GB | +| GC gen0/gen1/gen2 | ~105/50/9 | ~70/44/8 | + +- Wall time is unchanged within noise: the retarget alone buys no speed. +- Allocation drops by about a third. The profile below shows why; it is not the BCL. + +## 2026-10-21: profile of the same benchmark (net10 FCS) + +One dotTrace timeline snapshot for the overview, and dotnet-trace for the split by phase: CPU samples +(`dotnet-sampled-thread-time`) plus GC allocation ticks (`Microsoft-Windows-DotNETRuntime:0x1:5`). +The allocation ticks sum to 11.1 GB, which matches the `--timings` figure. + +### CPU by phase + +Running samples only, attributed by the innermost phase on the stack. "Fable → FCS" is FCS code +that Fable calls through the symbol API, which FCS runs lazily during Fable's transform. + +| Phase | Share of running CPU | +| --- | --- | +| FCS parse | 22% | +| FCS type check | 21% | +| Fable's own code | 13% | +| Fable → FCS symbol API | 10% | +| Fable → FCS `FSharpExprConvert` | 5% | +| FCS import (TcImports, IL reading) | 4% | +| Other (mostly a blocked read of the MSBuild cracker's stdout) | 21% | + +- **About 40% of the parse share is waiting.** Parallel parse threads hit `FSharp.Compiler.Parser`'s static + constructor (the fsyacc tables) together and spin in `InitClassSlow` while one thread runs it: 8.5% of + running samples under parsing, 10.5% across all phases. +- **GC suspension shows up as running time**: `Thread.PollGC` is about 13% of running samples across phases. +- **JIT is not on the critical path.** The timeline shows 19.8 s of jitting over 65,725 methods, but a + ReadyToRun publish (FCS 19 → 38 MB) compiles in the same wall time, 3 runs each (8.0–8.6 s JIT, + 8.1–8.4 s R2R). The jitting is tier-up on idle cores. This rules out R2R as a speedup here. + +### Allocation by phase + +| Phase | Allocated | +| --- | --- | +| FCS type check | 3.2 GB (29%) | +| Fable → FCS `FSharpExprConvert` | 2.9 GB (26%) | +| Fable → FCS symbol API | 2.3 GB (21%) | +| Fable's own code | 1.5 GB (14%) | +| FCS import | 0.8 GB (8%) | +| FCS parse | 0.1 GB (1%) | + +- **1.95 GB (18% of all allocation) is one bug.** `FSharpExprConvert.GetWitnessArgs` runs on every value + or member application. It calls `ConstraintSolver.CodegenWitnessesForTyparInst`, which builds a fresh + `InfoReader` through `CreateCodegenState`, and that preallocates a dozen `Cache`s, each backed by a + `ConcurrentDictionary` of about 1,000 buckets. The `InfoReader` is built before checking whether any + type parameter has a trait constraint, and almost none do. Fix candidate (fickle-fsharp, + `src/Compiler/Checking/ConstraintSolver.fs`): return `[]` early when + `GetTraitConstraintInfosOfTypars g typars` is empty. The same `CreateCodegenState` sits under + IlxGen's and the Optimizer's witness paths. +- The rest of the FCS-side allocation is spread thinly: type-structure hashing (`TypeHashing.emitTType`), + `FSharpType.isUnresolved`, `CurriedParameterGroups`, and IL metadata rows read via tuples. + +### Why the net10 build allocates 5 GB less + +The current Fable build, run against the old netstandard2.0 `lib/fcs` binaries (only the FCS assemblies +swapped), reproduces 16.0 GB. Traced, the difference is two closures that the old binary allocates and the new one does not: + +- `primEntityRefEq@471`: 3.4 GB (`TypedTreeBasics.fs`, called by every entity-ref equality test) +- `arrayPathEq` `loop@397`: 1.07 GB (the inner `let rec loop` over the path arrays) + +Neither function has platform-specific code. Both binaries are optimised builds, but closure names differ +(`loop@235-42` vs `loop@233-47`), so the old binary is a different source revision and possibly a +different compiler. Which of the two removed the closures has not been isolated. +`arrayPathEq` is still a candidate for a span-based `SequenceEqual` rewrite. + +### Witness fix applied (fickle-fsharp `7687bf609`) + +`CodegenWitnessesForTyparInst` now returns no witnesses before building the codegen state when no +type parameter has a trait constraint. 3 runs: + +| | Before | After | +| --- | --- | --- | +| Compilation total (wall) | 8.6–10.1 s | 8.0–8.6 s | +| Allocated | ~11.0 GB | ~8.7 GB | +| GC gen0/gen1/gen2 | ~70/44/8 | ~59/17/6 | + +Generated output is unchanged: `tests/Js/Main` JavaScript is byte-identical (141 files), and +`tests/Python` is identical once the nondeterministic `_exprN`/`_arrowN`/`ObjectExprN` counters are normalised (110 files). +Warnings are identical. Compiler tests pass 133/133. + +### Wall-clock view: type checking is serial + +Sample timestamps per phase, from the same trace (ms from trace start): + +| Phase | Wall span | Threads | +| --- | --- | --- | +| Project cracking (`run_dotnet_msbuild`) | 0–3,543 | 5 | +| FCS parse, all 76 files (`ParseInput`) | 4,755–5,135 (0.4 s) | 24 | +| FCS type check (`TypeCheckOneInputEntry`) | 5,138–13,976 (8.8 s) | **1** | +| Fable transform (`transformFile`) | 6,147–14,031 | 8 | + +- Type checking is the critical path, and it runs on one thread, file after file. Fable's transforms + finish 55 ms after the last file is checked. +- Parsing is a 0.4 s burst. Its large CPU share is 24 threads at once, and the parser static constructor + costs only 60–75 ms cold (measured in isolation). Removing that contention would save at most ~0.1 s + of wall time, so it is not worth a fork change. + +### How much graph-based checking could save + +`fsc` on the same project (`dotnet build tests/Js/Main --no-dependencies --no-incremental`), with +`--times:`, with and without `--test:GraphBasedChecking`. Spans of `CheckDeclarations.CheckOneImplFile`, 69 files: + +| Mode | Type check wall | Summed per-file time | Effective parallelism | +| --- | --- | --- | --- | +| Sequential | 8.3 s | 8.3 s | 1.0 | +| Graph | 2.0 s | 13.9 s | 6.9 | + +- The dependency graph of the test project is wide: type checking wall time drops by 4x. +- Per-file times rise by ~70% under contention, and the floor is `ConvertTests.fs` (1.3–1.6 s on its own). +- `fsc` checks with the same type checker as Fable's `service_slim`, so this bounds what parallel checking + there could save on a cold compile: about 6 s of the ~8.5 s compilation. + +### Parallel type checking applied (fickle-fsharp `ac0a7b8c5`) + +A cold `ParseAndCheckProject` now checks files over the dependency graph +(`CheckMultipleInputsUsingGraphModeWithStates`), when the check cache is empty and the project has not +opted out of graph checking. Files reach Fable in project order, each as soon as it and every file before it are +checked: releasing them in dependency order instead made Fable's output non-deterministic (external +`DllRef/Lib.js` vs `DllRef_1/Lib.js` naming) and broke a root-module lookup that expects earlier files to be known. +The check cache gets the same per-file states a sequential check leaves, so watch-mode rechecks stay sequential +from the last unchanged file. + +| Benchmark | Before | After | +| --- | --- | --- | +| `tests/Js/Main` → JS, compilation total (wall) | 7.4 s | 3.0–4.1 s (3 runs) | +| `tests/Python` → Python, compilation total (wall) | 5.6 s | 2.9 s | +| Allocated (JS) | ~8.8 GB | ~9.2 GB | + +- JS output is byte-identical (141 files) and Python is identical after counter normalisation (110 files); + warnings are identical. Compiler tests pass 133/133. +- Watch mode: after editing `StringTests.fs`, the recheck (0.9 s) matches a cold watch-mode compile of the same + source, file for file. + +### Project cracking is cached + +The 2.6–4.3 s of project cracking in the tables above is with `--noCache`. Without it, Fable reads the cracked +project from its cache: 119 ms when nothing changed, 113 ms after editing a source file. A full crack only happens +on the first run or after a project file changes, so it needs no fork work. + +### Symbol API caching (fickle-fsharp `7d83faa66`) + +A fresh trace of the parallel-checking build ranked the FCS symbol API calls Fable makes by allocation. The top ones +recomputed the same answer on every call: + +- Every `FSharpType` predicate (`IsTupleType`, `HasTypeDefinition`, `IsAbbreviation`, ...) first checked resolution + by building an `FSharpEntity`; `IsAbbreviation` did so two or three times per call (408 MB). +- `FSharpType.GenericArguments` built a list, an array and a collection per call (192 MB, 5.8% of CPU), and Fable + reads it several times per type. +- `CurriedParameterGroups` (586 MB) is read several times per member by Fable. + +`FSharpType` now caches its resolution and generic arguments and checks the definition reference directly; +`FSharpMemberOrFunctionOrValue` caches its parameter groups. + +| | Before | After | +| --- | --- | --- | +| Allocated, `tests/Js/Main` → JS | 9.26 GB | 7.94 GB | +| Allocated, `tests/Python` → Python | 8.58 GB | 7.52 GB | +| Fable per-file work (summed, JS) | 13.9 s | 12.2 s | +| Compilation total (wall, JS, median of 6) | 3.42 s | 3.34 s | + +- Output is identical (JS byte-for-byte, 141 files; Python after counter normalisation, 110 files), as are warnings. + Compiler tests pass 133/133. +- Wall time does not move on this machine: FCS type checking (2.7–3.0 s) is the critical path, and Fable's work + runs beside it on spare cores. The CPU and allocation savings should show as wall time on machines with fewer cores. + +### Dependents-only recheck in watch mode (fickle-fsharp `fff58bb81`) + +Every check of several files now goes through the graph checker. Each file's result is kept and reused while the +file and everything it depends on (in the current or the previous dependency graph) are unchanged, so an edit +rechecks only the edited file and its dependents, in parallel. + +One watch session of `tests/Js/Main`, four edits in a row; "Fable compilation finished" time per edit: + +| Edit | Sequential recheck (before) | Dependents-only recheck | +| --- | --- | --- | +| Add a value to `StringTests.fs` (few dependents) | 0.94 s | 0.61 s | +| Add a value to `Util/Util.fs` (most files depend on it) | 3.6 s | 1.3 s | +| Type error in `StringTests.fs` | 0.39 s | 0.46 s | +| Revert both files | 3.7 s | 1.1 s | + +- Each recheck's output is identical to the sequential recheck's, file for file, and the type error is reported the + same way. +- Compared with a cold compile of the same sources, both rechecks differ only in `MiscTests.js` whenever it is + rechecked: FCS gives closures of a `let rec` a new numbered name (`recursive1@385-2`) each time a file is checked + again in the same process. This is existing behaviour, not new. +- Cold compiles are unchanged (byte-identical JS, identical warnings). Compiler tests pass 133/133. + +### Next measurements + +- The same tables for Python and for a large external project. +- (Dropped) Parser static-constructor contention: initialise the parser tables once, before the parallel parse starts. +- The same table for Python and for a large external project. + +### Profiling notes + +- `dottrace start` and `dotnet-trace collect -- ` both hang this benchmark. Project cracking + starts `dotnet msbuild`, which apparently inherits the profiler's suspend-on-start diagnostic port and waits forever. + Start Fable normally and attach by PID instead (`dotnet-trace collect -p `). +- Rider's dotTrace MCP tools time out on a 530 MB timeline snapshot of this run. diff --git a/notes/perf-frontiers.md b/notes/perf-frontiers.md new file mode 100644 index 0000000000..67cba4982b --- /dev/null +++ b/notes/perf-frontiers.md @@ -0,0 +1,698 @@ +# Performance frontiers + +Fickl controls the Fable compiler, FCS (fickle-fsharp `fickle_slim`), and the .NET 10 host configuration. +FSharp.Core is also an optimization frontier, but the 0.5.3 tool currently resolves its runtime from +NuGet (`FSharp.Core/10.1.203`), not the older unsigned DLL in `lib/fcs`. This file lists every place +left to look, not only the ones the current profile points at, so priorities can be set from measurements. + +The FSharp.Core discussed here is the compiler's runtime. User projects type-check against the +FSharp.Core they reference from NuGet. A runtime fork must preserve the actual shipped assembly +identity, API and behavior: Fable plugins also load into the compiler process and bind against it. + +Status: done, open (not tried), measured-no-gain, or ruled out (with the reason). + +Fickl's NuGet release uses `dotnet run --project src/Fickl.Build -- publish` with `FICKL_NUGET_KEY` +provided in the environment (`--pack-only` builds and validates without uploading). The single +`FicklVersion` property in `src/Fickl.Branding.targets` versions Fickl.Core, Fickl.AST, Fickl.Compiler, +Fickl.PublishUtils and the CLI packages together, while retaining upstream assembly versions for binary +compatibility with existing plugins. EasyBuild's changelog-derived package version is overridden after its +version target runs. Generated runtime libraries are rebuilt before packing, and +every tool runtime package is checked for all bundled language libraries. Companion packages and all runtime packages are pushed before +the tool pointer package. Compiler symbols are embedded; its empty separate symbol archive is not pushed. + +The initial 0.5.2 release incorrectly applied the package version to companion assembly versions, which +broke existing Feliz plugins. The published 0.5.3 correction preserves those assembly identities and restores +the missing Rust and Dart runtime payloads. Validation includes 141 compiler tests, the assembly-qualified +AST reference used by older plugins, a consumer of all four companion packages, and a Fantomas tools +compile using its existing Feliz plugins. Fickl.Core is the separate `src/Fickl.Core` project containing +Fickl-specific attributes, not a renamed Fable.Core package; its consumer check resolves +`Fickl.Core.CaseTestAttribute`. The mistaken rename in 0.5.2 also caused duplicate project identities when +restoring the JS suite. All 13 corrected packages were published on 2026-10-22. NuGet rejected the +publishing key's unlist request with HTTP 403; 0.5.2 remains listed by the maintainer's decision. + +Runtime sweeps use cold `--noCache` runs, configurations interleaved, 4–5 runs each, and compare the compile phase +("Fable compilation finished in"), which excludes project cracking. Glutinum's compile times range 4.2–6.8 s within one +configuration, so it is not used to rank settings; the JS test suite stays within ±0.2 s. + +## End to end against upstream (Fickl 0.5.3, 2026-10-22) + +[Comparison image](benchmarks/fickl-0.5.3.png), [raw timings](benchmarks/fickl-0.5.3.csv), +[package hashes and environment](benchmarks/fickl-0.5.3-manifest.json). Wall clock from process start to exit, +five interleaved runs per configuration, fresh output directories and `--noCache`, including restores. +Cache hits are warmed; the CSV separately records warmups and one first-use cache build per workload. +All 120 measured runs succeeded. These measurements compare the full fork with upstream, not just the +incremental effect of the IWSAM optimization. + +| Project | Fable 5.22 | Fable, telemetry opted out | Fickl, cache off | Fickl, cache hit | Hit vs opted out | +| --- | --- | --- | --- | --- | --- | +| Sutil | 9.20 s | 7.28 s | 3.99 s | 2.39 s | 3.0× | +| Fantomas tools | 7.62 s | 6.29 s | 2.68 s | 1.92 s | 3.3× | +| Ionide | 12.22 s | 5.60 s | 2.91 s | 2.84 s | 2.0× | +| JS suite (`--exclude Fable.Core`) | 28.28 s | 8.87 s | 4.07 s | 3.94 s | 2.3× | +| Python bench | 11.24 s | 9.89 s | 7.43 s | 1.80 s | 5.5× | +| BEAM bench | 10.58 s | 9.17 s | 7.09 s | 1.52 s | 6.0× | + +The previous harness was recovered from the local Claude scratchpad. Its Windows replacement is +`notes/benchmarks/benchmark-0.5.3.py --scratch --tool `; +`plot-0.5.3.py` renders the CSV with matplotlib. The JS workload was resumed after correcting the +Fickl.Core project identity; packaged tool files were verified byte-identical after repacking. +Cleanup stopped leftover MSBuild workers and verified none remained; active Rider project evaluations +are excluded because Rider immediately launches replacements. Telemetry/network overhead varies by +machine and session, so the previous day's default-upstream timings are not reused. + +## End to end against upstream (Fickl 0.5.1, 2026-10-21) + +Same harness, adding upstream with `DOTNET_CLI_TELEMETRY_OPTOUT=1` set by the user (Fable as installed waits about 9 s +on telemetry per `dotnet` process here; Fickl 0.5.1 opts its own processes out). Wall clock, median of 5 +(`harness/fair.tsv`, image `harness/fickl-0.5.1.png`): + +| Project | Fable 5.22 | Fable 5.22, opted out | Fickl, package cache off | Fickl, cache hit | Hit vs opted out | +| --- | --- | --- | --- | --- | --- | +| Python bench | 17.5 s | 12.4 s | 9.0 s | 3.1 s | 4.0× | +| BEAM bench | 15.5 s | 11.6 s | 8.9 s | 3.0 s | 3.9× | +| Sutil | 18.2 s | 9.8 s | 5.7 s | 3.8 s | 2.6× | +| Fantomas tools | 14.5 s | 8.4 s | 4.2 s | 2.9 s | 2.9× | +| Ionide | 18.6 s | 7.5 s | 4.8 s | 4.0 s | 1.9× | +| JS suite (`--exclude Fable.Core`) | 50.8 s | 13.4 s | 7.0 s | 5.3 s | 2.5× | + +Project loading on a hit is now 0.6–1.9 s (was 2.2–6.8 s; JS suite 28 s). + +## End to end against upstream (Fickl 0.4.0, 2026-10-21) + +The compile phase overstates what a user sees: project cracking and process startup are outside it. Wall clock from +process start to exit, `--noCache`, the installed 0.4.0 win-x64 tool package against upstream Fable 5.22, interleaved +in one session, median of 5 (`harness/fairbench.sh`, results in `harness/fair.tsv`): + +| Project | Fable 5.22 | Fickl, package cache off | Fickl, cache hit | Cracking (both) | +| --- | --- | --- | --- | --- | +| Python bench | 12.9 s | 9.7 s | 3.7 s (3.5×) | 2.0 s | +| BEAM bench | 11.7 s | 9.3 s | 3.6 s (3.2×) | 2.1 s | +| Sutil | 11.7 s | 7.3 s | 5.1 s (2.3×) | 2.8 s | +| Fantomas tools | 8.8 s | 4.5 s | 4.0 s (2.2×) | 2.0 s | +| Ionide | 10.4 s | 7.7 s | 7.9 s (1.3×) | 5.4 s | + +A cache miss costs 9.7–19.8 s once (compile plus building the entry). Glutinum and `tests/Js/Main` failed to compile +with both compilers in this setup (Glutinum's own `CssModules.fs`; `Fable.Core.Quotations` for the test suite outside +the build script), so they are left out. On a hit, cracking is now the largest part of every project's time. + +## 1. Host and runtime configuration (no source changes) + +| Lever | Status | Notes | +| --- | --- | --- | +| Server GC | done | Already set upstream. | +| `TieredCompilation.CallCountingDelayMs=0` | done | Fork setting. | +| ReadyToRun FCS | superseded | Same wall time on its own; jitting runs on idle cores. See the composite row. | +| GC gen0 budget (`GCgen0size=256 MB`) | measured-worse | JS suite compile 3.6 → 5.2 s despite 49 → 21 gen0 GCs. | +| DATAS off (`GCDynamicAdaptationMode=0`) | measured-no-gain | 49 → 4 GCs, compile 3.60 → 3.47 s (noise). GC count is not what costs time. | +| Concurrent (background) GC off | measured-no-gain | Within noise. | +| Invariant globalization | measured-no-gain | Within noise on the JS suite. | +| TieredPGO off (alone) | measured-mixed | Compile phase, median of 5: JS suite 3.83 → 3.19 s, Python suite 3.16 → 2.62 s, Sutil 4.88 → 4.63 s, Ionide 3.02 → 3.06 s, Fantomas tools 4.47 → 4.85 s (slower in every run). Not made the default. | +| TieredPGO off + DATAS off | measured-mixed | Best on four projects, Fantomas tools 4.47 → 5.22 s. | +| Full JIT up front (`TieredCompilation=0`) | measured-worse | JS suite 3.83 → 3.39 s, Ionide 3.02 → 3.90 s. Exposed an output-dir race, fixed in `cbd6e3fc7`. | +| QuickJitForLoops off (with PGO off) | measured-no-gain | No better than PGO off alone. | +| Native AOT | open, low | Reflection-heavy (FCS, plugins); JIT is off the critical path. | +| Composite ReadyToRun + TieredPGO off | done | Per-runtime tool packages (see below). | + +### Composite ReadyToRun with TieredPGO off + +With the package cache most compilations are short, and JIT becomes a large part of them: a Python cache hit spent +5.4 s of JIT (summed over threads) on a 2.2 s compile phase. `--timings` now prints process JIT time and CPU time, +which stay comparable while other work loads the machine (it did during these runs: 50–65% CPU from other processes). + +Variants, interleaved, median of 4–6 runs, compile phase (process CPU in brackets): + +| Workload | IL (shipped 0.3.0) | IL, PGO off | Composite R2R | Composite R2R, PGO off | +| --- | --- | --- | --- | --- | +| JS suite, `--noPackageCache` | 4.19 s (39.6) | 3.34 s (32.0) | 4.91 s (45.8) | 3.18 s (29.4) | +| Fantomas tools, `--noPackageCache` | 3.73 s (18.2) | 3.53 s (16.0) | 4.11 s (17.2) | 2.78 s (13.3) | +| Fantomas tools, cache hit | 1.61 s (12.3) | 1.50 s (10.9) | 1.09 s (9.1) | 0.90 s (7.7) | +| Python bench, cache hit | 2.60 s (7.1) | 2.27 s (5.6) | 1.19 s (4.4) | 1.06 s (3.7) | +| Sutil, cache hit | 2.76 s (18.2) | 2.62 s (15.8) | 3.27 s (18.6) | 2.33 s (13.0) | + +Second set, IL against composite R2R with PGO off: Ionide hit 3.42 → 2.38 s, BEAM bench hit 1.95 → 0.90 s, Python +bench `--noPackageCache` 9.0 → 8.5 s; Glutinum hit 6.9 → 5.7 s and `--noPackageCache` 4.7 → 5.9 s, with CPU lower in +both (Glutinum's spread within one configuration is wider than these differences). + +- R2R alone loses on long compilations: hot R2R methods are rejitted through an instrumented tier before the optimised + one, so they run slow code for longer. With PGO off they go straight to the optimised tier. +- Per-assembly R2R without FCS (`PublishReadyToRunExclude`) kept most of the JIT: FCS is where the startup JIT is. +- Composite (one image of the app's assemblies, framework outside it) beat per-assembly R2R: crossgen can inline + FSharp.Core into FCS and Fable within the image. +- Shipping: `dotnet pack` builds one package per runtime (`win-x64`, `win-arm64`, `linux-x64`, `linux-arm64`, + `linux-musl-x64`, `osx-x64`, `osx-arm64`), each one composite R2R, a portable IL `any` package, and the `Fickl` + pointer package; the .NET 10 SDK installs the one for the machine (`src/Fickl.Branding.targets`). The SDK turns + composite off for framework-dependent apps, so a target sets it again before crossgen runs. Each runtime package is + ~28 MB (IL package 13.5 MB). Publish the runtime packages before the pointer package. +- Output of the installed `win-x64` package is identical to an IL source compile (Fantomas tools). The other runtime + packages were built (crossgen cross-compiles) but not run here. + +## 2. FSharp.Core (compiler runtime) + +**Status: partially explored.** Paused at the user's request after the TypeToken-array +experiment below. No experimental Core or FCS replacement has been adopted. + +### Cold loading and startup checkpoint (2026-10-22) + +Before moving to FSharp.Core, the packaged 0.5.3 win-x64 executable was measured in fresh +processes, with five measured runs after warmup. This is cold Fickl project-option loading +(`--noCache`), not cold OS pages or an empty NuGet cache. Package caching was disabled. +All comparison runs produced identical JS within each workload. + +| Workload / mode | Process wall median ms | Project loading median ms | +| --- | --- | --- | +| `--version` | 130 | — | +| Tiny project, restore | 1,455 | 799 | +| Tiny project, `--noRestore` | 1,195 | 562 | +| JS suite, restore | 4,383 | 1,559 | +| JS suite, `--noRestore` | 4,149 | 1,252 | + +The version path is only a lower-bound startup probe: it does not initialize everything needed +to compile. Process wall includes the RTK launcher. Restore and MSBuild evaluation are material +costs, but removing them would change behavior for edited imports, framework overrides and +package state. `--noRestore` remains an explicit caller choice, not a proposed default. + +A separate interleaved runtime experiment set `DOTNET_TieredPGO=0` for the process tree. +The packaged compiler already sets TieredPGO false in its runtime configuration; this explored +the runtime work of its MSBuild/build subprocesses without omitting any targets or restore. +Tiny loading was 799 → 807 ms and JS loading 1,527 → 1,579 ms. Process wall medians were +1,428 → 1,440 ms and 4,295 → 4,467 ms respectively. There is no measured improvement, so +no environment default or loading behavior was changed. A larger loader redesign is not +justified by this limited experiment; these results do not close all cold-loading frontiers. + +Raw timings, tiny fixture and drivers are in `notes/benchmarks/cold-start/`. The drivers use +the existing validated 0.5.3 package under `temp/release-0.5.3/verified-package/`; run them from +the worktree root. Watch latency remains deferred. The next priority is FSharp.Core changes +that reduce measured FCS work while preserving assembly identity and semantics. + +| Lever | Status | Notes | +| --- | --- | --- | +| Target net10.0 instead of netstandard2.x | open | Unlocks span/vectorised BCL paths under `String`, `Array`, hashing. | +| Generic equality / comparison / hashing fast paths | open | FCS leans on structural equality; check its share in a profile. | +| `Map` / `Set` implementation | open | Used throughout FCS name-resolution environments. | +| `List` / `Seq` / `Array` hot functions | open | Profile-driven. | + +### Runtime source alignment prerequisite (2026-10-22) + +The first Core experiment added only `AggressiveInlining` hints to `MapTree.mk` and +`MapTree.rebalance`. The algorithm, comparator calls, insertion order and balancing rules were +unchanged. The local Core source built successfully for netstandard2.1; the unchanged upstream +MapModule/MapType tests passed all 49 cases for both baseline and candidate in an isolated +test project. The full upstream test project could not reach tests because its dependency-manager +project failed with the existing FS0074 missing `System.Array` reference error. + +Before a timing comparison, the FCS process rejected the replacement Core DLL. Inspection found: + +- The tool's `fable.deps.json` resolves `FSharp.Core/10.1.203`, with signed assembly identity + `10.1.0.0`, public-key token `b03f5f7f11d50a3a`. +- `lib/fcs/FSharp.Core.dll` and the sibling checkout's default build are unsigned `10.0.0.0`. +- The CLI disables its implicit Core reference, but Thoth.Json.Core 0.10.0, + Thoth.Json.Core.Auto 0.2.0 and Thoth.Json.System.Text.Json 0.5.0 require Core 10.1.203 + transitively. That package wins dependency resolution over the vendored reference. + +An isolated public-signed build with version 10.1.0.0 allowed an identity-matched API inspection; +this was not adopted as a release configuration. The shipped Core exposed 3,156 reflected API +entries versus 3,140 in the local source build. The differences are in the LINQ `AnonymousObject` +types: the shipped types are sealed and override equality/hashing; the older source types do not. +A concrete probe confirmed that two `AnonymousObject(42)` instances compare equal with the +shipped runtime and unequal with the local build. The map inlining hints are not responsible +for this difference; the baseline source is older. Merely changing version/signing would therefore +introduce an unrelated behavioral regression. + +The package nuspec identifies its source as dotnet/dotnet commit +`c23858a6d860abe3ca94f9ac630c3e61b2625664`; its +[MutableTuple.fs](https://raw.githubusercontent.com/dotnet/dotnet/c23858a6d860abe3ca94f9ac630c3e61b2625664/src/fsharp/src/FSharp.Core/MutableTuple.fs) +contains the newer equality/hashing implementation. **Align Core source to the runtime package +before pursuing or shipping a Core optimization.** Preserve all runtime behavior, not just reflected +API entries; this inspection is a useful gate, not an exhaustive compatibility proof. + +No Core performance result is claimed. The inlining changes were removed from source and no +vendored or packaged runtime DLL was replaced. The existing unsigned local build output was +restored. `notes/benchmarks/core-runtime/` saves the API delta, rejected experiment patch and +an executable equality probe. From the worktree root: + +```powershell +rtk dotnet run --project notes/benchmarks/core-runtime -c Release -- temp/release-0.5.3/verified-package/tools/net10.0/win-x64/FSharp.Core.dll temp/core-runtime-api.txt true +``` + +Passing the older local build as the first argument with expected result `true` must fail. +This prerequisite was subsequently resolved by the source-aligned experiment below. + +### Source-aligned Core baseline and map experiment (2026-10-22) + +The VMR source manifest maps the runtime package to dotnet/fsharp commit +`7d0437e8bcf9ead9483483eaa556f1998af8806b`. The Core source directory's Git object IDs +were compared with the VMR directory at the package commit: every entry matched. +An isolated checkout of that commit builds netstandard2.1 Core with assembly version +10.1.0.0 and the runtime public key. Public signing is only for local experiments; +this is not a release configuration or a Microsoft-signed replacement. + +The saved preparation script was run successfully from the worktree root: + +```powershell +rtk proxy python notes/benchmarks/core-runtime/prepare-aligned-core.py +``` + +It requires an unchanged pinned checkout, builds the baseline, checks its assembly identity, +compares all 3,156 reflected API entries with the packaged runtime, checks anonymous-object +equality, and records hashes and source commits in `temp/core-runtime-baseline/manifest.json`. +These are targeted compatibility checks, not an exhaustive equivalence proof. + +The two `AggressiveInlining` hints on `MapTree.mk` and `MapTree.rebalance` were then rebuilt +on this aligned source. Both baseline and candidate passed all 49 unchanged upstream +MapModule/MapType tests and all ten saved full compiler diagnostic/attribute snapshots. +The broader upstream Core test project was blocked by an FS0074 dependency build error +in FSharp.DependencyManager.Nuget; no full-suite pass is claimed. + +Five interleaved measured fresh processes after warmups, identical CLI/FCS IL with only Core +changed, `--noCache --noRestore --noPackageCache`: + +| Workload | Compile median ms, baseline → candidate | Process wall median ms | Allocation median MB | +| --- | --- | --- | --- | +| Binding | 1,794 → 1,811 (+0.9%) | 3,494 → 3,486 | 1,220 → 1,219 | +| JS suite | 2,176 → 2,273 (+4.5%) | 4,185 → 4,342 | 7,813 → 7,812 | + +Every run produced byte-identical JS (67 binding files and 141 JS-suite files). +[Raw results](benchmarks/core-runtime/aligned-map-comparison.csv) include round zero warmups. +The hints did not establish a useful gain and were removed. No production source, vendored +binary or packaged runtime was changed. These are IL experiments, not R2R release timings. +The next Core lead should come from allocation/call profiles of this aligned runtime; +plugin compatibility and broader Core tests remain gates before adopting a replacement. + +### Aligned Core allocation and call profiles (2026-10-22) + +Two fresh processes per workload were traced with sampled thread time and GC allocation ticks. +The experimental CLI/FCS IL baseline is the same as the map comparison above (its executable +banner still reports 0.5.1); this is not the packaged 0.5.3 R2R executable. Its loaded Core file +is byte-identical to the verified aligned baseline, SHA-256 +`0d62ab9aed739975537e55caa4536a9e8fc7a2fcf11d9e6cb5135e23cfda5b9e`. + +The analyzer selects stacks containing CheckExpressions, CheckDeclarations or +PostInferenceChecks. CPU summaries count only `CPU_TIME`, exclude unmanaged/wait samples, +and exclude Core frames above the innermost FCS frame so compiler callbacks are not charged +to collection or async wrappers. These are sampled sums across threads, not elapsed time or +predicted speedups. Allocation ticks estimate allocated bytes by sampled type and stack; +they are not exact object counts or retained heap sizes. All four traces report zero lost +events; each JS trace has one allocation tick without a stack, excluded from checking totals. + +| Lead | Binding, two runs | JS suite, two runs | Interpretation | +| --- | --- | --- | --- | +| Entity-name table map-node allocations | 166.4 / 166.0 MiB | 3.2 / 4.1 MiB | Rebuilding persistent name tables in FCS dominates this binding-specific allocation lead. | +| `TypeToken` boxing under structural array hashing/equality | 2.5 / 1.8 MiB | 143.9 / 145.9 MiB | A repeatable Core/FCS boundary cost for the JS workload. | +| `MapTree.mk` sampled self time | 135 / 153 ms | 187 / 161 ms | Allocation/construction is visible; the previously rejected inlining hints did not solve it. | +| Generic arbitrary-array hashing/equality sampled inclusive time | not a leading cost | 514 / 706 ms | Includes runtime work below Core; the two operations' totals are added here, not nested Core helper totals. | + +Total estimated checking-stack allocation was 1,080 / 1,081 MiB for binding and +3,909 / 3,903 MiB for JS. The final profiled outputs match the preceding baseline outputs +byte for byte: 67 binding JS files and 141 JS-suite files. No production changes were made. + +The most focused next experiment is typed structural hashing/equality for FCS's struct +`TypeToken[]` boundary. `Array.GetValue` boxes its elements today. Replacing it with ordinary +.NET equality would not establish equivalence: retain F# element equality semantics, hash +combination and truncation, element traversal order, and any supplied comparer behavior. +A general Core array fast path must additionally preserve nonzero lower bounds, ranks, +null handling, NaN ER/PER behavior, short-circuiting, and exception order. Characterize these +before implementing a candidate. The entity-name map lead is primarily an FCS construction +strategy investigation; mutating existing map nodes would violate persistent-map semantics. + +Saved [profile summaries](benchmarks/core-runtime/profile-results.json), collection driver +`notes/benchmarks/core-runtime/profile.ps1`, CPU analyzer and allocation parser make the +investigation repeatable. The CPU analyzer's synthetic check verifies callback exclusion, +wait exclusion, checking scope and self-time accounting. Raw traces stay in `temp/core-profile`. +The driver attaches by PID: launching the compiler under dotnet-trace stalled its MSBuild +children in this environment, so that abandoned run was stopped and is not included. + +### TypeToken-array experiment — partially explored, paused (2026-10-22) + +The experiment specialized only the FCS type-subsumption cache comparer. The initial +wrapper preserved the ordinary structural comparer's hash results and equality over +23,785 checks (788 keys, 47 token values), including all token cases, Stable/Unstable tags, +both coercion flags, null arrays, and lengths around the 19-element hash prefix. All ten +diagnostic/attribute snapshots matched; all benchmark runs produced identical JS (67/141 files). +Missing-candidate and deliberately corrupted-hash checks failed as intended. + +Five measured runs per variant after warmups gave allocation medians of 1,219 → 1,218 MB +for binding and 7,800 → 7,721 MB for JS. **No speedup is established.** Background language +servers were active, and binding compile times alone varied from about 2 to 10 seconds. +[Raw wrapper results](benchmarks/type-token-arrays/wrapper-comparison.csv) are retained for +allocation/output evidence, not reliable timing comparison. + +Source review found that the wrapper could leave generated hashing on Core's arbitrary-array +path. A direct typed comparer was drafted to avoid that path and cache-key boxing, but its +build was stopped when the user paused this frontier. It is **unvalidated**, saved only as +an experiment patch. The original source and local FCS output DLL were restored; neither +vendored binaries nor packages changed. + +The expanded harness additionally checks all boundary-array pairs and default-key exception +behavior; it passed 62,594 checks against the initial wrapper. A default TTypeCacheKey with null structures throws NullReferenceException when +hashed by both the baseline and initial wrapper. The unvalidated direct draft's null handling +would return a hash instead: this is an observable behavior difference to resolve before any +future adoption, even though normal cache-key construction does not produce that value. + +Resume from `notes/benchmarks/type-token-arrays/`: characterize exception behavior first, +validate a corrected direct comparer against the live structural oracle, then rerun allocation +and end-to-end measurements under quieter conditions. Broader compiler/plugin and R2R +validation remain outstanding. Do not treat either saved patch as release-ready. + +## 3. FCS + +### Where type checking goes on real projects + +`FICKL_FCS_ACTIVITIES=` with `--timings` (`23634f260`) records FCS's per-file check activities. Cold `--noCache` +compiles, one run each; "package" files are the sources Fable copies into `fable_modules` and checks ahead of the +project's own files. + +| Project | Check wall | Package files, summed | Own files, summed | ≤1 file in flight | Largest packages | +| --- | --- | --- | --- | --- | --- | +| Fantomas tools | 5.0 s | 56 files, 6.6 s | 42 files, 1.0 s | 3.5 s | Feliz 3.9 s, Elmish 0.8 s, Fable.Promise 0.6 s | +| Sutil | 3.7 s | 60 files, 8.0 s | 60 files, 2.7 s | 1.6 s | Fable.Formatting.Markdown 1.7 s, Feliz.Engine.Bulma 1.5 s, Feliz.Engine 1.3 s | +| Ionide | 2.6 s | 14 files, 2.5 s | 32 files, 1.8 s | 1.7 s | Fable.Promise 1.1 s, VS Code bindings 0.6 s | +| Glutinum | 4.7 s | 37 files, 1.8 s | 66 files, 5.2 s | 3.5 s | Fable.Promise 1.1 s, FsToolkit 0.7 s | +| `tests/Js/Main` | 2.3 s | 10 files, 2.1 s | 66 files, 17.0 s | 0.1 s | — | + +- Package sources are most of the type-checking work on three of the four external projects, and they are identical + on every run of a given package version. In Fantomas tools the serial stretch is Feliz's own file chain + (`Styles.fs` → `Properties.fs` → `React.fs` → `Html.fs` → `JSX.fs`). +- `fsc` checks Fantomas tools' own 48 files in 1.3 s summed with a 0.37 s critical path; the gap to Fable's 5.0 s is the + package sources, not slower checking. +- Fable.Promise costs 0.6–1.1 s for 2–4 files wherever it is used. +- The JS test suite has almost no package sources and a wide graph, which is why it gained most from parallel checking. + +| Lever | Status | Notes | +| --- | --- | --- | +| Cache type-checked package sources across runs | done | `src/Fable.Cli/PackageCache.fs`; results below. | +| Package sets that cannot be one assembly | done | Sutil's packages declare both a module and a type `Feliz.Util`; FCS type-checks that but cannot emit it. Such sets are now cached as one assembly per package (`Fable.Precompiled..dll`, each referencing the ones before it), with one merged precompiled info. Sutil compile 4.8 → 2.1 s on a hit. | +| Package-like sources outside `fable_modules` | done | JS/TS: `paket-files` sources compiled ahead of the project's own files are cached with the package sources; their path relative to the project and whether there is an out dir are in the key, their output is stored apart (`/outside`), and they are left out when another source dir would map to the same out dir. Needed the anonymous record fix below. Ionide compile phase 2.77 → 2.15 s; output identical but for one local named after a cached member's parameter (`uri` for `options`). Python and BEAM not enabled: no `paket-files` project to check them on, and BEAM names modules from source paths. | +| Anonymous record types across the cached assembly | done | fickle-fsharp `c0e7f8143`. An anonymous record type belongs to its assembly, so a cached API taking an anonymous record no longer accepted the project's own (Ionide: "Two anonymous record types are from different assemblies"), and an overload taking one could silently lose to an `obj` overload. This affected the existing package cache too, for any package with such an API. `InteractiveChecker.SetAnonRecdTypesUnifiedAssembly` makes the project's and the cache's assemblies one for anonymous records (other assemblies unchanged), set on a hit. Edge cases checked against a source compile: an `obj`/anonymous overload pair, a struct anonymous record, a cached function returning one, an annotated binding and a copy-and-update. | +| Cache build blocks the watcher on a miss | open | ~3.5 s once per package set and options; could run in the background. | +| Files waiting on `Promise.fs` at the start of checking | explained | Fable.Promise's `PromiseImpl.fs` is a root-level `[] module` that calls `Promise.PromiseBuilder()`, so the graph correctly makes every later file depend on it and, through it, on `Promise.fs`. A package cache hit removes it: in Ionide checking starts at 0 ms instead of 1.0 s and the check wall goes 2.55 → 2.19 s; the critical path is then `Fable.Import.VSCode.fs` (1.07 s, from `paket-files`). | +| Fable.Promise check cost | open | 0.6–1.1 s per project; find what in it is slow to check. | +| IWSAM-as-type check collecting extension methods | done | fickle-fsharp `594a55f8b`. `CheckIWSAM` runs for every interface named in a signature and gathered all methods in scope, extension methods included, only to drop extension members: 44% of checking Glutinum's 18.7k-line `TypeScript.fs` (stock `fsc` spends the same 3.0 s on it). Now intrinsic methods only; same warnings. `TypeScript.fs` 3.2–3.5 → 1.9–2.0 s, Glutinum compile 4.4–5.0 → 3.4 s, Ionide 3.0–3.5 → 2.5 s, Sutil −0.2 s, JS suite unchanged; output identical. Upstreamable to dotnet/fsharp. | +| Large binding files: what remains | open | After the fix, `TypeScript.fs`'s profile is IL metadata reads for imported generic parameters (~12%), attribute checking (~12%), post-inference checks (~10%). | +| Codegen witness allocation | done | `7687bf609`. | +| Parallel cold type check over the file graph | done | `ac0a7b8c5`. Gain depends on how wide the project's graph is. | +| Symbol API caching | done | `7d83faa66`. | +| Watch mode: recheck only dependents | done | `fff58bb81`. | +| Watch mode: skip dependents when the edited file's signature is unchanged | done | fickle-fsharp `667b537e2`. Early cutoff: a changed file is checked again, and when a fingerprint of its signature (types with typars by position, constraints, attributes and their arguments, literal values, inline flags, argument names, accessibility, members and slots, unions, fields, exceptions; ranges and stamps left out; anything else makes it count as changed) equals the earlier one, its earlier finisher stays in the state, so dependents keep consistent entity identities and are reused, and only the new implementation is reported. JS suite, body edit in `Util.Testing.fs`: 57 files rechecked → 0, compile 1089 → 20 ms, edit to done 1315 → 249 ms (median of 6). Watch output matched a fresh compile in 29 scripted edits (`.fsi` pairs, SRTP, measures, abbreviations, AutoOpen, literals, CompiledName, inline bodies, anonymous records, type errors, inferred-only dependencies). Also fixes a 0.4.0 bug: when Fable checked part of the project and then all of it in one cycle, a dependent of a file the first check replaced was reused against the old result in the second (stale errors and output); results now record the generation of each dependency's finisher. `FICKL_FCS_SIGNATURE_CUTOFF=0` turns the cutoff off. | +| Critical path of the type check (deep chains, largest file) | open | Measure per project. | +| Type checker hot paths (name resolution, constraint solving) | open | Needs a profile on an external project. | +| Assembly import (`TcImports`, IL and pickled signature reading) | open | Grows with package count. | +| Persisted type check results across processes | open, large | Today every new process re-checks every file even when Fable's cache is warm. | +| `arrayPathEq` / entity-ref equality | open | Span-based compare. | +| Remaining allocation (`FSharpExprConvert`, type hashing) | open | 2.9 GB in `FSharpExprConvert` before the symbol caches. | +| Parser static-constructor contention | ruled out | At most ~0.1 s. | + +### Binding-file follow-up (2026-10-22, Windows) + +An isolated project checks Glutinum CLI's unchanged 18,721-line `src/Glutinum.Converter/TypeScript.fs` +at `acfa66e39c79d6a608e030b5f60fd6536596e190`, with Fable.Core 5.4.0 and Glutinum.Types 1.0.0-beta.2. +Local fixtures, scripts and raw measurements are under ignored `temp/binding-probe/`. + +Installed Fickl 0.5.1, `--noCache --noPackageCache`: the net10.0 fixture's first unprofiled run spent +1,476 ms in parse/check, 27 ms in Fable per-file work, and allocated 1,719 MB during compilation. +Attaching dotnet-trace by PID (avoiding inherited diagnostic-port suspension in MSBuild) gave these +inclusive sampled checking-stack shares. These are single profiles, not speedup measurements: + +| Fixture target | CheckIWSAM | Attribute checking | Post-inference checks | +| --- | --- | --- | --- | +| net10.0 | 22.1% | 15.0% | 13.3% | +| netstandard2.1, matching Glutinum | 21.3% | 16.1% | 16.9% | + +Generic-parameter metadata reading was not prominent in either isolated profile. This does not invalidate +the earlier full-project profile: the isolated project has fewer references and a different checking context. +The strongest remaining leads here are repeated intrinsic interface-member traversal and attribute checking. + +Dictionary insertion/locking also appeared hot, so two independent changes were tried against FCS +`667b537e2`. These comparisons used matching **IL** CLI builds, not the installed composite R2R package. +Five alternating runs per variant after warm-up, `--noCache --noRestore --noPackageCache`; binding fixture +targets net10.0. Values below are median compilation wall milliseconds (excluding cracking): + +| Experiment | Binding baseline → candidate | JS suite baseline → candidate | Allocation change | +| --- | --- | --- | --- | +| Call-local PropertyCollector: Dictionary instead of ConcurrentDictionary | 1,979 → 1,995 | 2,325 → 2,257 | Binding −7 MB; JS −36 MB | +| TranslateTopArgSynInfo: allocate/store only on a cache miss | 2,016 → 1,975 | 2,187 → 2,327 | Binding −1 MB; JS −2 MB | + +Neither was retained: the first did not improve the target binding workload, and the second's small binding +improvement did not carry across to the JS suite. Run-to-run spreads also caution against treating these +small differences as reliable wins. Every run produced byte-identical JS against its baseline (67 files +including the runtime for the binding fixture, 141 for the JS suite). This is output-equivalence evidence, +not a full compiler conformance test. + +Build/validation limitations: a fresh FCS build failed in the netstandard2.0 dependencies with FS0074 +(`System.Array` unavailable). Reusing existing dependency binaries and building FCS with +`BuildProjectReferences=false; ProduceReferenceAssembly=false` succeeded for baseline and candidates; +all three builds emitted the same four existing FS3261 symbol-API nullness warnings. The prebuilt compiler +test runner was stale relative to current source: its baseline failed `CaseTest in a TypeScriptTaggedUnion +does not emit warning`, `InlineInterface on a case without an interface field emits error`, and +`InlineInterface on a case with an interface field does not emit error`, referring to obsolete namespace +`Fickle` instead of current `Fickl.Core`. No passing-suite claim is made from that runner. + +### Static-abstract interface warning fast path (2026-10-22) + +The binding profile's `CheckIWSAM` subtree spent about half its samples filtering overrides and another +17% excluding hidden methods. Ordinary interfaces cannot trigger this warning, but previously paid for +those signature comparisons at every checked use. `InfoReader.HasIntrinsicStaticAbstractMethod` now +first scans the raw intrinsic method sets for a static dispatch slot. If none exists it returns false; +otherwise it runs exactly the previous override/hiding filters. It introduces no cache or invalidation rule. + +Source changes are in the sibling `fickle-fsharp` checkout (`fickle_slim`, commit `c32d52e92`), in +`InfoReader.fs`, `InfoReader.fsi`, and `Expressions/CheckExpressions.fs`. The rebuilt FCS DLL and XML are +vendored in `lib/fcs`. The dependency-build workaround described above was still required. + +Five alternating measured runs after warm-up, with cache and package cache disabled: + +| Build / workload | Median compile ms, baseline → candidate | Median allocation MB | +| --- | --- | --- | +| IL / binding | 1,963 → 1,773 (−9.7%) | 1,715 → 1,219 (−28.9%) | +| IL / JS suite | 2,197 → 2,250 (+2.4%; variable) | 7,815 → 7,811 | +| Composite R2R / binding | 1,467 → 1,242 (−15.3%) | 1,719 → 1,225 (−28.7%) | +| Composite R2R / JS suite | 1,933 → 1,979 (+2.4%; variable) | 7,819 → 7,819 | + +R2R binding process wall time fell 2,853 → 2,634 ms (−7.7%). JS-suite R2R paired compile deltas were +−8, −18, +108, +10, and −23 ms: these measurements support a binding-heavy workload improvement, not a +general compiler speedup. Every measured run had byte-identical JS to its baseline (68 binding output +files, including runtime and an existing helper, and 141 JS-suite files). The R2R outputs were regenerated +after clearing stale crossgen intermediates; the initial accidentally identical publishes were discarded. +Raw results: `temp/binding-probe/iwsam-il-comparison.csv` and `iwsam-r2r-comparison.csv`. + +The fresh compiler test runner passed all 140 tests, including seven warning cases: ordinary inherited +generic interfaces, repeated static-abstract uses, inherited slots, concrete static members, recursive +declarations, imported `IParsable`, and a concrete static member hiding an inherited slot. Baseline and +candidate diagnostic snapshots for these cases were identical. The previous stale runner failures above +do not recur after rebuilding. Static review found no actionable issue; diagnostic timing/location for +malformed imported metadata and type-provider callbacks have not been dynamically tested. + +The generated JS-suite output passed all 3,162 Node tests. The broader `test integration` command +stopped after 41/42 integration tests passed: `pathMap` expects a forward slash in `CallerFilePath`, +but Windows emits a backslash. The same focused test fails identically with the baseline FCS DLL. +No snapshot was changed. The command therefore did not reach its later project-configuration tests; +the separate 140-test compiler run above passed with the candidate DLL. + +### Next investigation: attribute constructor checking (2026-10-22) + +Watch latency remains deferred. Two fresh profiles of the unchanged binding fixture using the +validated 0.5.3 win-x64 package confirm attribute checking as the next focused investigation. +Both runs used `--noCache --noRestore --noPackageCache --timings` and attached dotnet-trace by PID. +Profiled parse/check wall times were 1,470 and 1,557 ms, with 1,223 MB allocated in each run. +These instrumented runs establish a profile, not an optimization benchmark. + +Attribute checking accounted for 18.0–18.1% of sampled checking stacks, post-inference checks +19.7–21.0%, and `CheckIWSAM` 6.8–8.0%. Within attribute checking, `TcMethodApplication` +accounted for 62–66% and `ResolveObjectConstructorPrim` 21–27%. Stable sorting appeared in +19–23% of attribute samples, with array cloning underneath it. These inclusive shares overlap. + +Next: trace constructor-group sorting and overload resolution to determine whether repeated +work can be avoided while preserving overload order, accessibility checks, and diagnostics. +Test any candidate against overloaded and named attribute arguments, then compare alternating +binding and JS-suite runs with output equivalence. No compiler change or speedup is claimed yet. +Cold project loading and process startup remain subsequent candidates; the previously rejected +dictionary experiments are not being repeated solely because locks still appear in profiles. + +Local traces and analysis are in ignored `temp/binding-probe/attributes-053*.speedscope.json`, +`profile.ps1`, and `attribute-profile.py`. Cleanup verified no leftover MSBuild workers after +the runs, excluding active Rider evaluations. + +#### Two-constructor stable-sort experiment: not retained + +`Item.MakeCtorGroup` sorts by total argument count before resolution. The candidate specialized +exactly two constructors: evaluate the first key and then the second once each, swap only for +`firstCount > secondCount`, otherwise preserve the original order. All other group sizes used +the existing stable sort. This preserved tie order, accessibility filtering, and key-evaluation +order; it added no cache. Removing sorting or using an unstable sort would change the intent +and was not attempted. Method groups were left unchanged. + +Matched Release IL FCS builds from `c32d52e92`, five interleaved measured runs plus warmups, +`--noCache --noRestore --noPackageCache` ([raw results](benchmarks/attribute-constructors/comparison.csv)): + +| Workload | Compile median ms, baseline → candidate | Process wall median ms | Allocation median MB | +| --- | --- | --- | --- | +| Binding | 2,220 → 2,158 (−2.8%) | 4,354 → 4,434 | 1,219 → 1,219 | +| JS suite | 2,443 → 2,514 (+2.9%) | 4,725 → 4,973 | 7,806 → 7,809 | + +Binding baseline compile times ranged 1,943–3,327 ms and candidate 1,938–2,314 ms. +The results do not establish a useful improvement, so the source change was removed and no +vendored compiler binary was replaced. Every measured run produced byte-identical JS to its +baseline (68 binding files, 141 JS-suite files). These were IL experiments, not release-package +R2R measurements; no claim is made about an R2R speedup. + +The saved characterization harness in `notes/benchmarks/attribute-constructors` covers ascending +and descending constructor arity, equal-arity overloads, named properties, private constructors, +bad arguments and arity, ambiguity, three constructors, and imported attributes. It compares +complete diagnostic snapshots (including duplicate errors, order, text and ranges), plus emitted +attribute constructor and named arguments from the checked assembly signature. All ten cases +matched for baseline and candidate. A deliberately altered expected diagnostic signature was +rejected, verifying the snapshot comparison is active. This is not a compiler mutation test or +exhaustive proof of semantic equivalence. No full compiler suite was run for this rejected candidate. + +Run the saved harness against the vendored baseline: + +```powershell +rtk dotnet run --project notes/benchmarks/attribute-constructors -c Release -- temp/attribute-observed.txt notes/benchmarks/attribute-constructors/baseline.txt +``` + +Both FCS builds used `BUILDING_USING_DOTNET=true`, `BuildProjectReferences=false`, and +`ProduceReferenceAssembly=false`; both reported the same four existing FS3261 nullness warnings. +The next investigation should distinguish actual repeated constructor-resolution work from +sorting samples before expanding the optimization scope. Watch latency remains deferred. + +#### Constructor-resolution instrumentation: cache not justified + +Temporary instrumentation of `ResolveObjectConstructorPrim` measured lookup, default-constructor +handling/accessibility filtering, and constructor-group creation separately. Three fresh IL +processes checked the binding fixture after one uninstrumented baseline run. Each recorded +4,591 successful constructor groups, comprising 3,757 distinct `(type name, source file, line)` +locations. Every recorded type was imported (`IsLocalRef = false`). Line-based locations may +combine different attributes on the same line; they are not proof of identical checking context. + +| Instrumented run | Lookup ms | Filtering/default handling ms | Group creation/sort ms | +| --- | --- | --- | --- | +| 1 | 35.156 | 34.863 | 9.110 | +| 2 | 10.514 | 13.262 | 6.376 | +| 3 | 11.453 | 13.964 | 15.867 | + +The repeated bulk work is small: 910 `AllowNullLiteralAttribute` lookups cost 0.6–1.2 ms +per run, and 749 `EmitAttribute` lookups cost 0.4–1.2 ms. `AbstractClassAttribute` accounts for +a large part of the first run's lookup/filter time despite only 12 calls. Timers include +first-use/JIT, GC and scheduling effects; stderr logging is outside the timed sections but +still perturbs execution. These measurements are diagnostic evidence, not speedup predictions. +They do not support adding a constructor cache or treating the earlier sampling share as +removable work. The measurement covers successful ordinary constructor-group construction; +delegate and no-constructor early returns are not included. + +The repeated type-attribute checks have documented intent. `TcTyconDefnCore_Phase1B_EstablishBasicKind` +first checks against broad `TyconDecl` targets to infer the type kind, then rechecks against the +actual class/struct/interface/enum target under `EnforceAttributeTargets`. Separately, +`TcAttributesCanFail` retries failures after members in a recursive group become available. +Skipping repeated source locations could therefore lose target errors, change duplicate-error +behavior, or retain an early failed lookup. Caching raw imported constructors would still need +to preserve source-range-specific metadata diagnostics, accessibility and type instantiations; +the measured benefit does not justify that added complexity. + +All 67 JS files in a fresh output directory were byte-identical between the baseline and all +three instrumented runs (the earlier 68-file directory contained an existing extra helper). +The ten saved attribute cases also matched full diagnostics and checked attribute arguments. +Instrumentation was removed from compiler source, and the local build output DLL was restored +to the matching uninstrumented baseline. No vendored binary was changed. + +[Measurements](benchmarks/constructor-resolution/measurements.json) and the +[temporary instrumentation patch](benchmarks/constructor-resolution/instrumentation.patch) +are saved for audit. Timers use `Stopwatch.GetTimestamp`; frequency was verified as 10,000,000 Hz. +Local driver and raw stderr are under ignored `temp/binding-probe/ctor-resolution/`. +This closes constructor lookup/caching as a low-priority lead on this fixture; it does not rule +out other workloads or the broader argument-checking and post-inference costs. + +### Experiment: Fantomas tools against precompiled packages + +A stub project with Fantomas tools' package references and one empty file, `fable precompile`d (9.3 s, once), then +Fantomas tools compiled with `--precompiledLib` pointing at it. Cold `--noCache`, 3 interleaved runs: + +| | Without | With precompiled packages | +| --- | --- | --- | +| FCS parse + type check (wall) | 3.47–3.75 s | 0.90–0.99 s | +| Compilation total (wall) | 3.73–4.06 s | 1.06–1.17 s | + +- The project's own 40 JS files have identical bodies; only import paths differ, because the package JS lives in the + precompiled folder (`../../pc-fantomas/fable_modules/...`) instead of `./fable_modules/...`. +- Pointing `--precompiledLib` at the output folder itself does not work: a fresh crack wipes `outDir/fable_modules` + before the precompiled info is read. + +How upstream's precompile works (for building on it): + +- `fable precompile` runs a normal compile, then writes `fable_modules/Fable.Precompiled.dll` (FCS `Compile` over every + source), chunked inline-expression JSON, and `precompiled_info.json` (Fable version, options, source → root module and + JS out path) (`Main.fs` 1675–1741, `Util.fs` 878–997). +- `--precompiledLib ` references the DLL and drops the listed sources, matching package sources by their path from + `fable_modules` on (`ProjectCracker.fs` 824–888); inline bodies load lazily (`State.fs` 579–590); imports of + precompiled files rewrite to their JS out path, JavaScript only (`Pipeline.fs` 122–129). +- Limits: one precompiled lib per compilation; Fable version must match exactly, but defines, FSharp.Core and Fable.Core + versions are not checked; source changes only warn (timestamp). + +### Package cache (`PackageCache.fs`) + +On by default for JavaScript, TypeScript, Python and BEAM; `--noPackageCache` turns it off, `FICKL_PACKAGE_CACHE` moves it +(default `%LOCALAPPDATA%/fickl/packages`, `~/.local/share/fickl/packages` on Linux). + +- Key: Fickl and Fable versions, compiler options, fable-library path, exclude/replace, the F# options with each + referenced assembly's size and timestamp, and every package source's path from `fable_modules` on and its content. +- Miss: after a successful compile, an FCS compile of the package sources alone (under its own project name, so its + signature data is not named like the consumer's) plus their inline bodies and JS, written to a temporary folder and + moved into place. Package sets FCS cannot emit as one assembly are compiled again as one assembly per package + (listed in `/groups.txt`, named apart because FCS identifies an assembly by its name); only when that fails + too is the key marked `.uncacheable`. +- Hit: reference the cached assembly, drop the package sources, copy their JS into `outDir/fable_modules`, rewrite out + paths and the fable-library dir, and point inline bodies at this project's copies of their files, so imports resolve + as in a source compile. +- Two upstream precompile bugs found on the way: overloads taking `Func<_, _>` and the lambda it wraps get the same + unique name, and `fable precompile` keeps whichever inline body comes last in completion order; the cache keeps the + last one in file order, as a source compile does. + +Cold `--noCache` compile phase, one run each, output compared file by file with a source compile (identical for all +but Sutil, where one eta-expanded lambda's parameter is named after the cached member's parameter, `value_2` instead of +`children`): + +| Project | Package files | Miss (compile + one-time cache build) | Hit | +| --- | --- | --- | --- | +| Fantomas tools | 52 | 3.1–3.9 s + 3.5–3.9 s | 1.01–1.25 s | +| Glutinum | 37 | 4.2 s + 1.5 s | 3.7 s | +| Ionide | 10 | 2.6 s + 0.9 s | 2.3 s | +| `tests/Js/Main` | 1 | 3.1 s + 0.4 s | 3.2 s | +| Sutil | 31 | 4.8 s + 6.6–7.4 s (11 assemblies) | 2.1 s | + +Also checked: runs without `--noCache` (cached project options, a skipped up-to-date compile, an edit), a hit after +the folder that built the entry was deleted, and watch mode (miss, then hit: identical output, 240–275 ms rebuilds). + +#### Python and BEAM + +- Python: imports of a cached file resolve through its out path in this project (`Pipeline.fs`, `PythonFileWriter`), + as JS does, because the entities of a cached assembly record the source path where the entry was built. A Python + library's `__init__.py` files are written on a hit. The project's output type is in the key: Python declares a + library's `__all__` in package sources too. +- BEAM: module names come from a source's path within its package, so imports need no rewriting. The module-name + clash check also covers cached package sources. +- Package `.fsi` files are type-checked but compile to nothing; they no longer stop an entry from being saved (this + affected JS too, e.g. FSharp.Data.Adaptive). +- Output checked against a `--noPackageCache` compile for a miss, a hit from another folder and a hit in place: BEAM + byte-identical; Python identical once the `_arrowN` / `ObjectExprN` numbering is ignored, which also differs + between two source compiles (a counter shared by files compiled in parallel). A hit from another folder can order + Python imports differently: they are sorted by their raw path, which for cached entities is the builder's. Python + output ran with the same results; BEAM output was not run (no Erlang on the machine). +- Python test suite: 2602 passed. + +Bench projects (scratchpad): Python = FsToolkit.ErrorHandling, Hedgehog, Thoth.Json.Core, Fable.Python (104 package +files); BEAM = FsToolkit.ErrorHandling, Hedgehog (65). Cold `--noCache`, 5 alternating runs, median: + +| Target | Upstream Fable 5.22: wall / compile phase | Fickl, no package cache | Fickl, cache hit | +| --- | --- | --- | --- | +| Python | 16.2 s / 13.7 s | 12.0 s / 9.3 s | 4.9 s / 2.1 s | +| BEAM | 14.3 s / 11.3 s | 12.3 s / 9.4 s | 5.1 s / 2.1 s | + +A miss costs a one-time 8–9 s entry build on top of the compile. + +## 4. Fable + +| Lever | Status | Notes | +| --- | --- | --- | +| Fable transforms and printing | open, low | Runs beside FCS on spare cores; not on the critical path with many cores. | +| Project cracking on a cold cache | partial | 2–5 s per first run (MSBuild design-time build); cached afterwards. The main project's TargetFramework query was a `dotnet msbuild` process ahead of the design-time build; when the project file states one unconditional TargetFramework the two now run side by side, and the design-time build reruns with the queried value if they differ (checked: a Directory.Build.targets override, multi-targeting, a conditional TargetFramework). Ionide 7.6 → 3.9 s, Fantomas tools and Sutil −0.5 s. References were cracked after the main build, level by level, one process each; the references a project file states directly (unconditional, no properties, one stated TargetFramework, up to one per core) now start beside the main build, and a result is used when the referencing build asks for the same TargetFramework and restore rewrote none of the assets and `nuget.g` files it read (`FICKL_CRACK_SPECULATION=0` turns it off). JS suite cracking 13.4 → 8.2 s, median of 5 interleaved, output identical; checked: cold restore (rejected, recracked), a Directory.Build.targets TargetFramework override (rejected), multi-targeting and conditional references (not speculated), `--exclude`. Remaining: restore on every cold run. | +| .NET CLI telemetry in the compiler's own `dotnet` processes | done | Each `dotnet msbuild` (and the `dotnet build` of an `--exclude`d project) sent telemetry on exit and waited for it: here about 9 s per process (on a VPN), against 0.6 s with `DOTNET_CLI_TELEMETRY_OPTOUT=1`. Those processes now get it unless the user set the variable; user `--run` commands are unchanged. JS suite load 27.4 → 1.9 s, wall 31.9 → 5.8 s, output identical. Upstream pays it too. | +| Watch mode fixed overhead | open | ~230–300 ms rebuilds on small edits; find what the floor is made of. | +| Process startup and assembly loading | open | Measure. | diff --git a/src/Directory.Build.targets b/src/Directory.Build.targets index 123c4c57fb..59ecc2055b 100644 --- a/src/Directory.Build.targets +++ b/src/Directory.Build.targets @@ -9,4 +9,5 @@ $(FSharpAnalyzersOtherFlags) --report "$(SarifOutput)$(MSBuildProjectName)-$(TargetFramework).sarif" $(FSharpAnalyzersOtherFlags) --exclude-analyzers PartialAppAnalyzer + diff --git a/src/Fable.AST/Fable.fs b/src/Fable.AST/Fable.fs index 9a6647954c..eb8df5206c 100644 --- a/src/Fable.AST/Fable.fs +++ b/src/Fable.AST/Fable.fs @@ -78,6 +78,7 @@ type UnionCase = abstract FullName: string abstract CompiledName: string option abstract UnionCaseFields: Field list + abstract Attributes: Attribute seq [] type Constraint = diff --git a/src/Fable.Cli/Entry.fs b/src/Fable.Cli/Entry.fs index a12b69be37..445d055592 100644 --- a/src/Fable.Cli/Entry.fs +++ b/src/Fable.Cli/Entry.fs @@ -71,6 +71,7 @@ let knownCliArgs () = "default is 'Debug' in watch mode, or 'Release' otherwise" ] [ "--verbose" ], [ "Print more info during compilation" ] + [ "--timings" ], [ "Print a per-phase timing and allocation report after each compilation" ] [ "--silent" ], [ "Don't print any log during compilation" ] [ "--typedArrays" ], [ "Compile numeric arrays as JS typed arrays (default is true)" ] [ "--watch" ], [ "Alias of watch command" ] @@ -88,6 +89,11 @@ let knownCliArgs () = [ "--yes" ], [ "Automatically reply 'yes' (e.g. with `clean` command)" ] [ "--noRestore" ], [ "Skip `dotnet restore`" ] [ "--noCache" ], [ "Recompile all files, including sources from packages" ] + [ "--noPackageCache" ], + [ + "Type-check package sources instead of reusing them from Fickl's package cache" + "(JavaScript and TypeScript; the cache is keyed by package contents and options)" + ] [ "--noGitignore" ], [ "Don't generate a `.gitignore` file in the `fable_modules` folder" @@ -505,7 +511,7 @@ let private logPrelude commands language = | "" -> "" | status -> $" (status: {status})" - Log.always ($"Fable {Literals.VERSION}: F# to {language} compiler{status}") + Log.always ($"Fickl {Fickl.Version.VERSION} (Fable {Literals.VERSION}): F# to {language} compiler{status}") match getLibPkgVersion language with | Some(repository, pkgName, version) -> @@ -572,6 +578,9 @@ let main argv = else LogLevel.Information, Verbosity.Normal + Timings.setEnabled (args.FlagEnabled "--timings") + PackageCache.setEnabled (not (args.FlagEnabled "--noPackageCache")) + // Override the logger now that we know the verbosity level Log.setLogger verbosity (createLogger level) diff --git a/src/Fable.Cli/Fable.Cli.fsproj b/src/Fable.Cli/Fable.Cli.fsproj index d1e50711e2..14c172b823 100644 --- a/src/Fable.Cli/Fable.Cli.fsproj +++ b/src/Fable.Cli/Fable.Cli.fsproj @@ -32,7 +32,9 @@ + + diff --git a/src/Fable.Cli/Main.fs b/src/Fable.Cli/Main.fs index 351d9ec0a3..6a872387ee 100644 --- a/src/Fable.Cli/Main.fs +++ b/src/Fable.Cli/Main.fs @@ -210,7 +210,9 @@ module private Util = if not (IO.Directory.Exists dir) then IO.Directory.CreateDirectory dir |> ignore - do! Pipeline.compileFile com cliArgs pathResolver isSilent outPath + do! + Pipeline.compileFile com cliArgs pathResolver isSilent outPath + |> Timings.timeFile return Ok @@ -407,6 +409,15 @@ type ProjectCracked(cliArgs: CliArgs, crackerResponse: CrackerResponse, sourceFi CrackerOptions(cliArgs, evaluateOnly) |> getFullProjectOpts resolver + Timings.recordCrack ms + + // fickl: reference cached package sources, or remember to cache them after this compilation + let result = + if evaluateOnly then + result + else + PackageCache.apply cliArgs result + // We display "parsed" because "cracked" may not be understood by users Log.always $"Project and references ({result.ProjectOptions.SourceFiles.Length} source files) parsed in %i{ms}ms{Log.newLine}" @@ -560,6 +571,7 @@ and FableCompiler(checker: InteractiveChecker, projCracked: ProjectCracked, fabl lastFile, ?subscriber = subscriber ) + |> Timings.timeCheck ) let updateProject (state: FableCompilerState) (files: FSharpImplementationFileContents list) = @@ -606,7 +618,11 @@ and FableCompiler(checker: InteractiveChecker, projCracked: ProjectCracked, fabl let! res = compileFile com projCracked.CliArgs state.PathResolver state.IsSilent let res = - if not projCracked.CliArgs.Precompile then + // fickl: the package cache needs the inline bodies of package sources + if + not projCracked.CliArgs.Precompile + && not (PackageCache.collectsInlineExprs fileName) + then res else res @@ -845,6 +861,8 @@ and FableCompiler(checker: InteractiveChecker, projCracked: ProjectCracked, fabl if not isSilent then Log.always "Started Fable compilation..." + Timings.reset () + let! results, ms = Performance.measureAsync <| fun () -> @@ -864,6 +882,7 @@ and FableCompiler(checker: InteractiveChecker, projCracked: ProjectCracked, fabl match results with | Ok results -> Log.always $"{Log.newLine}Fable compilation finished in %i{ms}ms{Log.newLine}" + Timings.report ms results | Error e -> e.Message + Log.newLine + e.StackTrace |> Fable.FableError |> raise @@ -889,7 +908,8 @@ and FableCompiler(checker: InteractiveChecker, projCracked: ProjectCracked, fabl [], assemblies, Log.log, - ?precompiledInfo = (projCracked.PrecompiledInfo |> Option.map (fun i -> i :> _)), + // fickl: cached package sources resolve inline imports from this project's fable_modules + ?precompiledInfo = (projCracked.PrecompiledInfo |> Option.map PackageCache.asPrecompiledInfo), getPlugin = Reflection.loadType projCracked.CliArgs ) @@ -984,7 +1004,19 @@ type State = // as some OS use case insensitive paths let importDir = importDir.ToLower() - this.DeduplicateDic.GetOrAdd(importDir, (fun _ -> set this.DeduplicateDic.Values |> addTargetDir)) + // fickl: choosing a target dir reads every dir chosen so far, and GetOrAdd runs its factory outside its + // own lock, so two files compiled at once could choose the same dir and write the same output file + match this.DeduplicateDic.TryGetValue importDir with + | true, targetDir -> targetDir + | false, _ -> + lock + this.DeduplicateDic + (fun () -> + this.DeduplicateDic.GetOrAdd( + importDir, + (fun _ -> set this.DeduplicateDic.Values |> addTargetDir) + ) + ) } static member Create(cliArgs, ?watchDelay, ?recompileAllFiles) = @@ -1535,6 +1567,8 @@ let private compilationCycle (state: State) (changes: ISet) = if cliArgs.CompilerOptions.Language = Beam then projCracked.SourceFiles |> Array.map (fun f -> f.NormalizedFullPath) + // fickl: package sources from the package cache still name modules this project's must not clash with + |> Array.append (PackageCache.cachedPackageSources ()) |> checkBeamModuleNames cliArgs let! fableCompiler = @@ -1718,6 +1752,29 @@ let private compilationCycle (state: State) (changes: ISet) = Log.always ($"Precompiled info saved in {ms}ms") return 0 + // fickl: cache the package sources compiled in this run for later compilations + | false, false when PackageCache.hasPending () -> + let! fableProj = fableCompiler.GetFableProject() + + let compiled = + fableResults + |> List.choose ( + function + | Ok res -> + Map.tryFind res.File fableProj.ImplementationFiles + |> Option.map (fun file -> + { + PackageCache.CompiledFile.File = res.File + PackageCache.CompiledFile.OutPath = res.OutPath + PackageCache.CompiledFile.RootModule = file.RootModule + PackageCache.CompiledFile.InlineExprs = res.InlineExprs + } + ) + | Error _ -> None + ) + + do! PackageCache.save cliArgs compiled + return 0 | _ -> return 0 } diff --git a/src/Fable.Cli/PackageCache.fs b/src/Fable.Cli/PackageCache.fs new file mode 100644 index 0000000000..f6c0a30904 --- /dev/null +++ b/src/Fable.Cli/PackageCache.fs @@ -0,0 +1,711 @@ +/// Fickl: a per-user cache of type-checked package sources, on unless --noPackageCache is passed. +/// Fable compiles the F# sources that packages ship (copied to fable_modules) on every cold compile, and type-checking +/// them is often most of the compilation. A package version's sources never change, so after a successful compile the +/// package sources are precompiled once, as `fable precompile` does: an assembly with their signatures, their inline +/// bodies and their compiled output. Later compiles with the same packages and options reference that assembly instead +/// of type-checking the sources again, and get the package output copied into their own fable_modules. +/// JavaScript, TypeScript, Python and Erlang/BEAM: the targets whose imports of precompiled files resolve in this +/// project (JS/TS/Python rewrite them to this project's copies, BEAM names modules by their path within the package). +module Fable.Cli.PackageCache + +open System +open System.Collections.Generic +open System.IO +open System.Security.Cryptography +open System.Text +open FSharp.Compiler.CodeAnalysis +open FSharp.Compiler.SourceCodeServices +open Fable +open Fable.AST +open Fable.Compiler +open Fable.Compiler.ProjectCracker +open Fable.Compiler.Util + +let mutable private enabled = true + +let setEnabled value = enabled <- value + +/// A cache entry to build once this compilation has finished. +type private Pending = + { + Key: string + PackageSources: string[] + Response: CrackerResponse + } + +let mutable private pending: Pending option = None +let mutable private pendingSources = HashSet() + +/// The cached entry in use. +type private InUse = + { + Info: PrecompiledInfoImpl + /// Every assembly of the entry, the main one first + DllPaths: string list + /// The fable_modules folder the entry was built in, if known + BuilderModules: string option + UserModules: string + /// Cached sources outside fable_modules, by their path where the entry was built + UserSources: IDictionary + } + +let mutable private inUse: InUse option = None + +/// The package sources the cached entry in use stands for, which this compilation does not compile. +let mutable private cachedSources: string[] = [||] + +let cachedPackageSources () = cachedSources + +/// The name of a cache entry's assembly, and the prefix of the others' (`Fickl.Packages.`) +let private cacheAssemblyName = "Fickl.Packages" + +let private root () = + match Environment.GetEnvironmentVariable "FICKL_PACKAGE_CACHE" with + | null + | "" -> + IO.Path.Combine(Environment.GetFolderPath Environment.SpecialFolder.LocalApplicationData, "fickl", "packages") + | dir -> dir + +let private isApplicable (cliArgs: CliArgs) = + enabled + && not cliArgs.Precompile + && Option.isNone cliArgs.PrecompiledLib + && not cliArgs.SourceMaps + && ( + match cliArgs.CompilerOptions.Language with + | JavaScript + | TypeScript + | Python + | Beam -> true + | _ -> false + ) + +let private isOutputOption (option: string) = + [ "-o:"; "--out:"; "--doc:"; "--refout:"; "--target:" ] + |> List.exists (fun prefix -> option.StartsWith(prefix, StringComparison.Ordinal)) + +/// Paket's GitHub file dependencies (`paket-files///...`), pinned by paket.lock and compiled as plain +/// project sources. +let private isPaketSource (file: string) = + file.Contains("/paket-files/", StringComparison.OrdinalIgnoreCase) + +let private isInModules (modulesDir: string) (file: string) = + file.StartsWith(modulesDir + "/", StringComparison.OrdinalIgnoreCase) + +/// The sources to cache: the package sources in fable_modules and, for JS/TS, the paket-files sources compiled ahead of +/// the project's own files. JS/TS imports of a cached file resolve through its out path, which for a source outside +/// fable_modules is derived from its path relative to the project, so that path is part of the key. +let private packageSources (cliArgs: CliArgs) (response: CrackerResponse) = + let modulesDir = Path.normalizeFullPath response.FableModulesDir + let files = response.ProjectOptions.SourceFiles |> Array.map Path.normalizeFullPath + let modules = files |> Array.filter (isInModules modulesDir) + + let paketApplies = + match cliArgs.CompilerOptions.Language with + | JavaScript + | TypeScript -> true + | _ -> false + + let prefix = + files + |> Array.takeWhile (fun file -> isInModules modulesDir file || isPaketSource file) + + let paket = prefix |> Array.filter (isInModules modulesDir >> not) + + if + not paketApplies + || Array.isEmpty paket + || (prefix |> Array.filter (isInModules modulesDir)).Length <> modules.Length + then + modules + else + // A cached file's out dir must be the one a source compile chooses: Fable deduplicates out dirs that two + // source dirs map to (`Imports.getTargetAbsolutePath`), and the cached files are not there to claim theirs + let projDir = IO.Path.GetDirectoryName cliArgs.ProjectFile + + let targetDir (file: string) = + // As `Imports.trimPath` + (Imports.getRelativePath projDir (IO.Path.GetDirectoryName file)) + .Replace("../", "") + .Replace("./", "") + .Replace(":", "") + .ToLowerInvariant() + + let paketDirs = paket |> Array.map targetDir |> set + let paketSet = HashSet(paket, StringComparer.OrdinalIgnoreCase) + + let clash = + files + |> Array.exists (fun file -> + not (paketSet.Contains file) + && not (isInModules modulesDir file) + && paketDirs.Contains(targetDir file) + ) + + if clash then + modules + else + prefix + +/// Everything that can change what the package sources compile to: the compiler, its options, the assemblies they are +/// checked against, and the sources themselves (by their path from fable_modules on, and their content). +let private computeKey (cliArgs: CliArgs) (response: CrackerResponse) (sources: string[]) = + use hash = IncrementalHash.CreateHash HashAlgorithmName.SHA256 + + let add (text: string) = + hash.AppendData(Encoding.UTF8.GetBytes(text + "\n")) + + add $"fickl {Fickl.Version.VERSION} fable {Literals.VERSION}" + add $"%A{cliArgs.CompilerOptions}" + add $"lib {cliArgs.FableLibraryPath} exclude %A{cliArgs.Exclude} replace %A{cliArgs.Replace}" + // Python declares a library's exports (__all__), in package sources too + add $"output {response.OutputType}" + + for option in response.ProjectOptions.OtherOptions do + if option.StartsWith("-r:", StringComparison.Ordinal) then + let file = FileInfo(option.Substring 3) + + if file.Exists then + add $"{option} {file.Length} {file.LastWriteTimeUtc.Ticks}" + else + add option + elif not (isOutputOption option) then + add option + + let modulesDir = Path.normalizeFullPath response.FableModulesDir + let projDir = IO.Path.GetDirectoryName cliArgs.ProjectFile + + // The out paths of sources outside fable_modules, and their imports, depend on whether there is an out dir + if sources |> Array.exists (isInModules modulesDir >> not) then + add $"outDir {cliArgs.OutDir.IsSome}" + + for source in sources do + if isInModules modulesDir source then + add (IO.Path.GetRelativePath(modulesDir, source).Replace('\\', '/')) + else + add ("project " + IO.Path.GetRelativePath(projDir, source).Replace('\\', '/')) + + hash.AppendData(File.ReadAllBytes source) + + Convert.ToHexString(hash.GetHashAndReset()).Substring(0, 32).ToLowerInvariant() + +let private entryModulesDir key = + IO.Path.Combine(root (), key, Naming.fableModules) + +/// Package sets that type-check but cannot be emitted as one assembly (e.g. two packages declaring a module and a type +/// with the same name in one namespace) are recorded, so later compilations do not try again. +let private uncacheableMarker key = + IO.Path.Combine(root (), key + ".uncacheable") + +/// Package sources that cannot be one assembly are cached as several, one per package, each referencing the ones before +/// it. The first sits in the entry's fable_modules, as for a single assembly; this file lists the others. +let private groupsFile (entry: string) = IO.Path.Combine(entry, "groups.txt") + +let private groupDllPath (entry: string) (index: int) = + // The assembly is named after its file, and assemblies referenced together need distinct names + IO.Path.Combine(entry, "groups", $"{Naming.fablePrecompile}.{index}.dll") + |> Path.normalizeFullPath + +/// Output of cached sources outside fable_modules (out paths relative to fable_modules that start with `../`), stored +/// apart so copying the entry's fable_modules cannot write outside the project's. +let private outsideDir (entry: string) = IO.Path.Combine(entry, "outside") + +let private upSegment = "_up_/" + +let private encodeOutside (relative: string) = + let mutable relative = relative + let mutable prefix = "" + + while relative.StartsWith("../", StringComparison.Ordinal) do + relative <- relative.Substring 3 + prefix <- prefix + upSegment + + prefix + relative + +let private decodeOutside (stored: string) = + let mutable stored = stored + let mutable prefix = "" + + while stored.StartsWith(upSegment, StringComparison.Ordinal) do + stored <- stored.Substring upSegment.Length + prefix <- prefix + "../" + + prefix + stored + +let private isCacheMetadata (modulesDir: string) (file: string) = + let relative = IO.Path.GetRelativePath(modulesDir, file).Replace('\\', '/') + + relative = IO.Path.GetFileName(PrecompiledInfoImpl.GetDllPath modulesDir) + || relative = IO.Path.GetFileName(PrecompiledInfoImpl.GetPath modulesDir) + || relative.StartsWith("inline_exprs/", StringComparison.Ordinal) + +/// Uses a cached entry for the project's package sources if there is one, otherwise remembers to build one after a +/// successful compilation. Returns the response to compile with. +let apply (cliArgs: CliArgs) (response: CrackerResponse) : CrackerResponse = + pending <- None + pendingSources <- HashSet() + inUse <- None + cachedSources <- [||] + + if not (isApplicable cliArgs) then + response + else + let sources = packageSources cliArgs response + + if Array.isEmpty sources then + response + else + let key = computeKey cliArgs response sources + let entryModules = entryModulesDir key + let userModules = Path.normalizeFullPath response.FableModulesDir + + let uncacheable = File.Exists(uncacheableMarker key) + + let cached = + if not uncacheable && File.Exists(PrecompiledInfoImpl.GetPath entryModules) then + try + let json = Json.read(PrecompiledInfoImpl.GetPath entryModules) + + // Out paths are stored relative to fable_modules: point them at this project's copies, and + // inlined package code at this project's fable-library + let json = + { json with + FableLibDir = response.FableLibDir + Files = + json.Files + |> Map.map (fun _ file -> + { file with + OutPath = + IO.Path.Combine(userModules, file.OutPath) |> Path.normalizeFullPath + } + ) + } + + for file in Directory.EnumerateFiles(entryModules, "*", SearchOption.AllDirectories) do + if not (isCacheMetadata entryModules file) then + let target = + IO.Path.Combine(userModules, IO.Path.GetRelativePath(entryModules, file)) + + Directory.CreateDirectory(IO.Path.GetDirectoryName target) |> ignore + File.Copy(file, target, true) + + // A Python library makes each folder of compiled files a package, as Pipeline does + if + cliArgs.CompilerOptions.Language = Python + && response.OutputType = OutputType.Library + then + let initFile = IO.Path.Combine(IO.Path.GetDirectoryName target, "__init__.py") + + if not (File.Exists initFile) then + File.WriteAllText(initFile, "") + + let entry = IO.Path.Combine(root (), key) + + let outside = outsideDir entry + + if Directory.Exists outside then + for file in Directory.EnumerateFiles(outside, "*", SearchOption.AllDirectories) do + let relative = + IO.Path.GetRelativePath(outside, file).Replace('\\', '/') |> decodeOutside + + let target = IO.Path.Combine(userModules, relative) |> IO.Path.GetFullPath + Directory.CreateDirectory(IO.Path.GetDirectoryName target) |> ignore + File.Copy(file, target, true) + + let groupDlls = + if File.Exists(groupsFile entry) then + File.ReadAllLines(groupsFile entry) + |> Array.filter (String.IsNullOrWhiteSpace >> not) + |> Array.map (fun relative -> + IO.Path.Combine(entry, relative) |> Path.normalizeFullPath + ) + |> Array.toList + else + [] + + for dll in groupDlls do + if not (File.Exists dll) then + failwith $"{dll} is missing" + + Some(PrecompiledInfoImpl(entryModules, json), groupDlls) + with e -> + Log.warning $"Ignoring the cached package sources in {entryModules}: {e.Message}" + None + else + None + + match cached with + | Some(info, groupDlls) -> + Log.always $"Using precompiled package sources ({sources.Length} files) from the package cache" + + // The cached assemblies' anonymous record types are the project's, as in a compile of the sources + let projectAssembly = + IO.Path.GetFileNameWithoutExtension response.ProjectOptions.ProjectFileName + + InteractiveChecker.SetAnonRecdTypesUnifiedAssembly(fun name -> + name = projectAssembly + || name = cacheAssemblyName + || name.StartsWith(cacheAssemblyName + ".", StringComparison.Ordinal) + ) + + // Sources are keyed by where the entry was built: /fable_modules//... + let builderModules = + info.Files + |> Seq.tryPick (fun (KeyValue(file, _)) -> + let marker = "/" + Naming.fableModules + "/" + let i = file.IndexOf(marker, StringComparison.OrdinalIgnoreCase) + + if i >= 0 then + Some(file.Substring(0, i + marker.Length - 1)) + else + None + ) + + let dllPaths = info.DllPath :: groupDlls + + // The key holds each outside source's path relative to the project, so the builder's file with the + // same path from its paket-files folder on is this project's + let userSources = + let paketPath (file: string) = + let i = file.LastIndexOf("/paket-files/", StringComparison.OrdinalIgnoreCase) + file.Substring(i).ToLowerInvariant() + + let byPaketPath = Dictionary() + + for file in sources do + if not (isInModules userModules file) then + byPaketPath.TryAdd(paketPath file, file) |> ignore + + let map = Dictionary(StringComparer.OrdinalIgnoreCase) + + for KeyValue(file, _) in info.Files do + let file = Path.normalizeFullPath file + + if isPaketSource file && not (file.Contains("/" + Naming.fableModules + "/")) then + match byPaketPath.TryGetValue(paketPath file) with + | true, userFile -> map[file] <- userFile + | _ -> () + + map :> IDictionary<_, _> + + inUse <- + Some + { + Info = info + DllPaths = dllPaths + BuilderModules = builderModules + UserModules = userModules + UserSources = userSources + } + + cachedSources <- sources + let sourceSet = HashSet(sources, StringComparer.OrdinalIgnoreCase) + + { response with + ProjectOptions = + { response.ProjectOptions with + OtherOptions = + Array.append + response.ProjectOptions.OtherOptions + [| for dll in dllPaths -> "-r:" + dll |] + SourceFiles = + response.ProjectOptions.SourceFiles + |> Array.filter (Path.normalizeFullPath >> sourceSet.Contains >> not) + } + PrecompiledInfo = Some info + } + | None when uncacheable -> + Log.verbose (lazy $"Package sources are not cacheable: {File.ReadAllText(uncacheableMarker key)}") + response + | None -> + pending <- + Some + { + Key = key + PackageSources = sources + Response = response + } + + pendingSources <- HashSet(sources, StringComparer.OrdinalIgnoreCase) + response + +/// The precompiled info to give Fable. Inline bodies from cached package sources record the file they were declared in +/// where the entry was built; Fable resolves their imports relative to that file, so point it at this project's copy, +/// as when the sources are compiled here. +let asPrecompiledInfo (info: PrecompiledInfoImpl) : Fable.Transforms.State.PrecompiledInfo = + let inner = info :> Fable.Transforms.State.PrecompiledInfo + + match inUse with + | Some used when obj.ReferenceEquals(used.Info, info) -> + { new Fable.Transforms.State.PrecompiledInfo with + member _.DllPath = inner.DllPath + member _.TryGetRootModule(file) = inner.TryGetRootModule(file) + + member _.TryGetInlineExpr(name) = + inner.TryGetInlineExpr(name) + |> Option.map (fun expr -> + match used.BuilderModules with + | Some builderModules when + expr.FileName.StartsWith(builderModules + "/", StringComparison.OrdinalIgnoreCase) + -> + { expr with FileName = used.UserModules + expr.FileName.Substring(builderModules.Length) } + | _ -> + match used.UserSources.TryGetValue(Path.normalizeFullPath expr.FileName) with + | true, userFile -> { expr with FileName = userFile } + | _ -> expr + ) + interface Fable.Transforms.State.PrecompiledAssemblies with + member _.DllPaths = used.DllPaths + } + | _ -> inner + +/// Whether the compilation needs a file's inline bodies to build a cache entry. +let collectsInlineExprs (file: string) = + pending.IsSome && pendingSources.Contains(Path.normalizeFullPath file) + +let hasPending () = pending.IsSome + +/// One compiled file: its source, JS out path, root module and inline bodies. +type CompiledFile = + { + File: string + OutPath: string + RootModule: string + InlineExprs: (string * Fable.InlineExpr)[] + } + +/// Compiles package sources to an assembly named `assemblyName`, against the project's references and `dllRefs`. +let private compileAssembly + (response: CrackerResponse) + (assemblyName: string) + (dllRefs: string list) + (sources: string[]) + (dllPath: string) + = + async { + // The F# signature data is named after the project file: under the consumer's own name, its references into + // the cached assembly would resolve against the project being compiled (wrong overloads, missing members). + // The assemblies of one entry need distinct names for the same reason + let projectFile = + IO.Path.Combine(IO.Path.GetDirectoryName response.ProjectOptions.ProjectFileName, assemblyName + ".fsproj") + + let options = + { response.ProjectOptions with + ProjectFileName = projectFile + SourceFiles = sources + OtherOptions = Array.append response.ProjectOptions.OtherOptions [| for dll in dllRefs -> "-r:" + dll |] + } + + let checker = InteractiveChecker.Create(options) + + let paths, sourceReader = + sources |> Array.map Fable.Compiler.File |> Fable.Compiler.File.MakeSourceReader + + return! checker.Compile(paths, sourceReader, dllPath) + } + +/// Splits package sources, in file order, into runs from the same package (its folder in fable_modules). +let private splitByPackage (modulesDir: string) (sources: string[]) = + let packageOf (source: string) = + let relative = IO.Path.GetRelativePath(modulesDir, source).Replace('\\', '/') + + match relative.IndexOf '/' with + | -1 -> relative + | i -> relative.Substring(0, i) + + let groups = ResizeArray>() + let mutable current = None + + for source in sources do + let package = packageOf source + + if current <> Some package then + groups.Add(ResizeArray()) + current <- Some package + + groups[groups.Count - 1].Add source + + groups |> Seq.map Seq.toArray |> Seq.toList + +let private errorSummary (diagnostics: FSharp.Compiler.Diagnostics.FSharpDiagnostic[]) = + diagnostics + |> Array.filter (fun d -> d.Severity = FSharp.Compiler.Diagnostics.FSharpDiagnosticSeverity.Error) + |> Array.truncate 5 + |> Array.map (fun d -> $"{Log.newLine} {d.FileName}({d.StartLine},{d.StartColumn}): {d.Message}") + |> String.concat "" + +/// Compiles the package sources as one assembly at the main DLL path or, when they cannot be one (see +/// uncacheableMarker), as one assembly per package, the first at the main DLL path. Returns the other assemblies' paths +/// relative to the entry, or the failure's message and compile errors. +let private compileAssemblies (response: CrackerResponse) (sources: string[]) (temp: string) (tempModules: string) = + async { + let mainDll = PrecompiledInfoImpl.GetDllPath tempModules + + let! diagnostics, result = + compileAssembly response cacheAssemblyName [] sources mainDll + + match result with + | None -> return Ok [] + | Some e -> + let errors = errorSummary diagnostics + + let groups = + splitByPackage (Path.normalizeFullPath response.FableModulesDir) sources + + if errors = "" || groups.Length < 2 then + return Error(e.Message, errors) + else + Log.verbose (lazy $"Package sources cannot be one assembly, caching them as {groups.Length}:{errors}") + + // A failed attempt may leave a partial assembly behind + if File.Exists mainDll then + File.Delete mainDll + + let rec compileGroups index (dlls: string list) groups = + async { + match groups with + | [] -> return Ok dlls + | group :: rest -> + let dllPath = + if index = 0 then + mainDll + else + groupDllPath temp index + + Directory.CreateDirectory(IO.Path.GetDirectoryName dllPath) |> ignore + + // A package only depends on packages before it + let dllRefs = + if index = 0 then + [] + else + mainDll :: (dlls |> List.map (fun relative -> IO.Path.Combine(temp, relative))) + + let! diagnostics, result = + compileAssembly response $"{cacheAssemblyName}.{index}" dllRefs group dllPath + + match result with + | Some e -> return Error(e.Message, errorSummary diagnostics) + | None -> + let dlls = + if index = 0 then + dlls + else + dlls @ [ IO.Path.GetRelativePath(temp, dllPath).Replace('\\', '/') ] + + return! compileGroups (index + 1) dlls rest + } + + return! compileGroups 0 [] groups + } + +/// Builds the pending cache entry from a successful compilation of every package source. The entry is written to a +/// temporary folder and moved into place, so concurrent compilations never see a partial entry. +let save (cliArgs: CliArgs) (compiled: CompiledFile list) = + async { + match pending with + | None -> () + | Some p -> + pending <- None + + let byFile = + compiled + |> List.map (fun f -> Path.normalizeFullPath f.File, f) + |> dict + |> Dictionary + + // Signature files are type-checked (and go into the assembly) but compile to nothing + let implementations = + p.PackageSources + |> Array.filter (fun source -> not (source.EndsWith(".fsi", StringComparison.OrdinalIgnoreCase))) + + match implementations |> Array.tryFind (byFile.ContainsKey >> not) with + | Some missing -> Log.verbose (lazy $"Not caching package sources: {missing} was not compiled in this run") + | None -> + let entry = IO.Path.Combine(root (), p.Key) + let temp = IO.Path.Combine(root (), $"{p.Key}.tmp-{Guid.NewGuid():N}") + let tempModules = IO.Path.Combine(temp, Naming.fableModules) + let userModules = Path.normalizeFullPath p.Response.FableModulesDir + + try + Log.always $"Caching package sources ({p.PackageSources.Length} files) for later compilations..." + + let! result, ms = + Performance.measureAsync + <| fun () -> + Directory.CreateDirectory tempModules |> ignore + compileAssemblies p.Response p.PackageSources temp tempModules + + match result with + | Error(message, errors) -> + Log.warning $"Could not cache package sources: {message}{errors}" + + // Only compile errors are permanent for a given key; anything else may succeed next time + if errors <> "" then + try + File.WriteAllText(uncacheableMarker p.Key, message + errors) + with _ -> + () + | Ok groupDlls -> + let files = + implementations + |> Array.map (fun source -> + let f = byFile[source] + let relative = IO.Path.GetRelativePath(userModules, f.OutPath).Replace('\\', '/') + + let outPath = + if relative.StartsWith("../", StringComparison.Ordinal) then + IO.Path.Combine(outsideDir temp, encodeOutside relative) + else + IO.Path.Combine(tempModules, relative) + + // Empty files are not written to disk + if File.Exists f.OutPath then + Directory.CreateDirectory(IO.Path.GetDirectoryName outPath) |> ignore + File.Copy(f.OutPath, outPath, true) + + f.File, + { + RootModule = f.RootModule + OutPath = relative + } + ) + |> Map.ofArray + + // Overloads can share a unique name (a Func<_, _> parameter and the lambda it wraps hash the + // same). A source compile keeps the last one declared, in file order, so keep that one too + let inlineExprs = + let byName = Dictionary() + + for source in implementations do + for name, expr in byFile[source].InlineExprs do + byName[name] <- expr + + byName |> Seq.map (fun kv -> kv.Key, kv.Value) |> Seq.toArray + + PrecompiledInfoImpl.Save( + files = files, + inlineExprs = inlineExprs, + compilerOptions = cliArgs.CompilerOptions, + fableModulesDir = tempModules, + fableLibDir = p.Response.FableLibDir + ) + + if not groupDlls.IsEmpty then + File.WriteAllLines(groupsFile temp, groupDlls) + + try + Directory.Move(temp, entry) + Log.always $"Package sources cached in {ms}ms" + with :? IOException when Directory.Exists entry -> + // Another compilation cached the same packages first + () + with e -> + Log.warning $"Could not cache package sources: {e.Message}" + + if Directory.Exists temp then + try + Directory.Delete(temp, true) + with _ -> + () + } diff --git a/src/Fable.Cli/Pipeline.fs b/src/Fable.Cli/Pipeline.fs index eaf00773b8..b40312acc3 100644 --- a/src/Fable.Cli/Pipeline.fs +++ b/src/Fable.Cli/Pipeline.fs @@ -199,7 +199,7 @@ module Python = let fileName = normalizeFileName targetPath Path.Combine(targetDir, fileName + fileExt) - type PythonFileWriter(com: Compiler, cliArgs: CliArgs, pathResolver, targetPath: string) = + type PythonFileWriter(com: Compiler, cliArgs: CliArgs, pathResolver: PathResolver, targetPath: string) = let stream = new IO.StreamWriter(targetPath) let projDir = IO.Path.GetDirectoryName(cliArgs.ProjectFile) let sourcePath = com.CurrentFile @@ -323,7 +323,26 @@ module Python = else path - if path.Contains('/') then + // fickl: a file precompiled by the package cache may have been compiled elsewhere, so the path its + // entities record is where it was compiled: import its out path in this project instead + let precompiledImport = + if path.Contains('/') && not isFableLibrary then + pathResolver.TryPrecompiledOutPath(IO.Path.GetDirectoryName(sourcePath), path) + |> Option.map (fun outPath -> + let targetDir = IO.Path.GetDirectoryName(targetPathForResolution) + let parts = (Imports.getRelativePath targetDir outPath).Split('/') + + if usePyPIForFableLibrary && parts |> Array.contains "fable_library" then + packagePath parts + else + relativePath parts + ) + else + None + + if precompiledImport.IsSome then + precompiledImport.Value + elif path.Contains('/') then // If inside fable-library then use relative path if isFableLibrary then "." + normalizeFileName path diff --git a/src/Fable.Cli/Timings.fs b/src/Fable.Cli/Timings.fs new file mode 100644 index 0000000000..8e113edd71 --- /dev/null +++ b/src/Fable.Cli/Timings.fs @@ -0,0 +1,161 @@ +/// Fickl: opt-in phase timings for a compilation, enabled with --timings. +/// Fable compiles files while FCS is still type-checking the project, so phases overlap: +/// per-file work is summed across threads and reported next to the wall-clock totals. +module Fable.Cli.Timings + +open System +open System.Diagnostics +open System.Threading +open Fable.Compiler.Util + +let mutable private enabled = false + +// FCS activities (parse, check and import, per file), as `fsc --times` reports them for the command-line compiler +let private fcsActivitiesFile = + match Environment.GetEnvironmentVariable "FICKL_FCS_ACTIVITIES" with + | null + | "" -> None + | file -> Some file + +let private fcsActivities = Collections.Concurrent.ConcurrentQueue() + +let private listenToFcs () = + let listener = + new ActivityListener( + ShouldListenTo = (fun source -> source.Name = "fsc"), + Sample = (fun _ -> ActivitySamplingResult.AllData), + ActivityStopped = + (fun a -> + let file = + match a.GetTagItem "fileName" with + | :? string as file -> file.Replace(',', ';') + | _ -> "" + + fcsActivities.Enqueue( + $"{a.OperationName},{a.StartTimeUtc.Ticks},%.3f{a.Duration.TotalMilliseconds},{Environment.CurrentManagedThreadId},{file}" + ) + ) + ) + + ActivitySource.AddActivityListener listener + +/// With FICKL_FCS_ACTIVITIES=, --timings also records FCS's own activities to that file. +let setEnabled value = + enabled <- value + + if value && fcsActivitiesFile.IsSome then + listenToFcs () + +// Counter cells, kept in an array so Interlocked can update them from compiling threads +[] +let private CrackMs = 0 + +[] +let private CheckMs = 1 + +[] +let private FileTicks = 2 + +[] +let private MaxFileTicks = 3 + +[] +let private FileCount = 4 + +[] +let private AllocatedAtStart = 5 + +let private counters: int64[] = Array.zeroCreate 6 +let mutable private gcAtStart = [| 0; 0; 0 |] + +let private gcCounts () = + [| GC.CollectionCount 0; GC.CollectionCount 1; GC.CollectionCount 2 |] + +/// Clears the per-compilation counters; project cracking is kept, it only happens once. +let reset () = + for i in CheckMs..FileCount do + Interlocked.Exchange(&counters[i], 0L) |> ignore + + counters[AllocatedAtStart] <- GC.GetTotalAllocatedBytes false + gcAtStart <- gcCounts () + +let recordCrack (ms: int64) = + if enabled then + counters[CrackMs] <- ms + +/// Times the whole-project FCS parse and type check. +let timeCheck (work: Async<'a>) : Async<'a> = + if not enabled then + work + else + async { + let sw = Stopwatch.StartNew() + let! res = work + Interlocked.Add(&counters[CheckMs], sw.ElapsedMilliseconds) |> ignore + return res + } + +let rec private updateMax (ticks: int64) = + let current = Volatile.Read(&counters[MaxFileTicks]) + + if + ticks > current + && Interlocked.CompareExchange(&counters[MaxFileTicks], ticks, current) <> current + then + updateMax ticks + +/// Times one file through FSharp2Fable, FableTransforms, the target transform, printing and writing. +let timeFile (work: Async<'a>) : Async<'a> = + if not enabled then + work + else + async { + let sw = Stopwatch.StartNew() + let! res = work + let ticks = sw.Elapsed.Ticks + Interlocked.Add(&counters[FileTicks], ticks) |> ignore + Interlocked.Increment(&counters[FileCount]) |> ignore + updateMax ticks + return res + } + +let private ticksToMs (ticks: int64) = TimeSpan(ticks).TotalMilliseconds + +/// Prints the timings table for a compilation that took totalMs of wall-clock time. +let report (totalMs: int64) = + if enabled then + let allocatedMb = + float (GC.GetTotalAllocatedBytes false - counters[AllocatedAtStart]) + / (1024.0 * 1024.0) + + let gcs = Array.map2 (-) (gcCounts ()) gcAtStart + let fileCount = counters[FileCount] + let filesMs = ticksToMs counters[FileTicks] + + let avgMs = + if fileCount = 0L then + 0.0 + else + filesMs / float fileCount + + let maxMs = ticksToMs counters[MaxFileTicks] + let nl = Environment.NewLine + + match fcsActivitiesFile with + | Some file -> + IO.File.WriteAllLines(file, Seq.append [ "name,startTicks,durationMs,thread,file" ] fcsActivities) + fcsActivities.Clear() + | None -> () + + Log.always ( + $"{nl}Timings (--timings):{nl}" + + $" Project cracking (wall) %8d{counters[CrackMs]} ms{nl}" + + $" FCS parse + type check (wall) %8d{counters[CheckMs]} ms{nl}" + + $" Fable per-file work (summed) %8.0f{filesMs} ms over %d{fileCount} files (avg %.1f{avgMs} ms, max %.1f{maxMs} ms){nl}" + + $" Compilation total (wall) %8d{totalMs} ms{nl}" + + $" Allocated %8.0f{allocatedMb} MB{nl}" + + $" GC collections gen0/gen1/gen2 %d{gcs[0]}/%d{gcs[1]}/%d{gcs[2]}{nl}" + // Process totals, cracking included: JIT cost and CPU time stay comparable on a busy machine + + $" JIT (process) %8.0f{Runtime.JitInfo.GetCompilationTime(false).TotalMilliseconds} ms over %d{Runtime.JitInfo.GetCompiledMethodCount false} methods{nl}" + + $" CPU time (process) %8.0f{Process.GetCurrentProcess().TotalProcessorTime.TotalMilliseconds} ms{nl}" + ) diff --git a/src/Fable.Compiler/MSBuildCrackerResolver.fs b/src/Fable.Compiler/MSBuildCrackerResolver.fs index a658d1f9d7..c6bf17b5ad 100644 --- a/src/Fable.Compiler/MSBuildCrackerResolver.fs +++ b/src/Fable.Compiler/MSBuildCrackerResolver.fs @@ -11,6 +11,117 @@ open Fable.AST open Fable.Compiler.Util open Fable.Compiler.ProjectCracker +/// fickl: the `dotnet msbuild` processes cracking starts outlived the compiler when it was stopped while they ran, and +/// one that never finished kept running. On Windows each one joins a job object that kills it when the compiler's +/// handle to the job closes, which happens however the compiler exits. Processes they start are left out of the job, +/// so MSBuild's reusable nodes keep their usual lifetime. Elsewhere they are killed when the compiler exits or is +/// interrupted. +module private ChildProcesses = + + open System.Runtime.InteropServices + + [] + type JobObjectBasicLimitInformation = + val mutable PerProcessUserTimeLimit: int64 + val mutable PerJobUserTimeLimit: int64 + val mutable LimitFlags: uint32 + val mutable MinimumWorkingSetSize: unativeint + val mutable MaximumWorkingSetSize: unativeint + val mutable ActiveProcessLimit: uint32 + val mutable Affinity: unativeint + val mutable PriorityClass: uint32 + val mutable SchedulingClass: uint32 + + [] + type IoCounters = + val mutable ReadOperationCount: uint64 + val mutable WriteOperationCount: uint64 + val mutable OtherOperationCount: uint64 + val mutable ReadTransferCount: uint64 + val mutable WriteTransferCount: uint64 + val mutable OtherTransferCount: uint64 + + [] + type JobObjectExtendedLimitInformation = + val mutable BasicLimitInformation: JobObjectBasicLimitInformation + val mutable IoInfo: IoCounters + val mutable ProcessMemoryLimit: unativeint + val mutable JobMemoryLimit: unativeint + val mutable PeakProcessMemoryUsed: unativeint + val mutable PeakJobMemoryUsed: unativeint + + [] + extern nativeint CreateJobObjectW(nativeint jobAttributes, nativeint name) + + [] + extern bool SetInformationJobObject(nativeint job, int infoClass, nativeint info, uint32 infoLength) + + [] + extern bool AssignProcessToJobObject(nativeint job, nativeint proc) + + let private JobObjectExtendedLimitInformationClass = 9 + let private JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK = 0x1000u + let private JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE = 0x2000u + + /// Never closed: the system closes it when the compiler exits. + let private job = + lazy + try + let job = CreateJobObjectW(0n, 0n) + + if job = 0n then + None + else + let mutable info = JobObjectExtendedLimitInformation() + + info.BasicLimitInformation.LimitFlags <- + JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE ||| JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK + + let length = Marshal.SizeOf() + let ptr = Marshal.AllocHGlobal length + + try + Marshal.StructureToPtr(info, ptr, false) + + if SetInformationJobObject(job, JobObjectExtendedLimitInformationClass, ptr, uint32 length) then + Some job + else + None + finally + Marshal.FreeHGlobal ptr + with _ -> + None + + let private running = ConcurrentDictionary() + + let private killRunning () = + for p in running.Values do + try + if not p.HasExited then + p.Kill(entireProcessTree = true) + with _ -> + () + + let private handlers = + lazy + AppDomain.CurrentDomain.ProcessExit.Add(fun _ -> killRunning ()) + Console.CancelKeyPress.Add(fun _ -> killRunning ()) + + /// Ties a started process to the compiler's lifetime until `release`. + let track (p: Process) = + if OperatingSystem.IsWindows() then + try + match job.Force() with + | Some job -> AssignProcessToJobObject(job, p.Handle) |> ignore + | None -> () + with _ -> + () + + handlers.Force() + running[p.Id] <- p + + let release (p: Process) = running.TryRemove p.Id |> ignore + module private MSBuildCrackerResolver = let private fsharpFiles = set [| ".fs"; ".fsi"; ".fsx" |] @@ -49,6 +160,9 @@ module private MSBuildCrackerResolver = // https://github.com/fable-compiler/Fable/issues/4014 psi.Environment.Add("DOTNET_NOLOGO", "1") + for key, value in Fable.Compiler.Util.Process.dotnetEnvVars () do // fickl + psi.Environment[key] <- value + if not (List.isEmpty defines) then let definesValue = defines |> String.concat ";" psi.Environment.Add("DefineConstants", definesValue) @@ -56,9 +170,11 @@ module private MSBuildCrackerResolver = use ps = new Process() ps.StartInfo <- psi ps.Start() |> ignore + ChildProcesses.track ps // fickl let output = ps.StandardOutput.ReadToEnd() let error = ps.StandardError.ReadToEnd() do! ps.WaitForExitAsync() + ChildProcesses.release ps // fickl return {| @@ -134,6 +250,9 @@ Exception: return tfs.Split ';' |> Array.head } + /// fickl: the restore outputs each design-time build read, by project file. + let restoreInputs = ConcurrentDictionary() + let mkOptionsFromDesignTimeBuildAux (referenceTargetFrameworks: ConcurrentDictionary) (isMain: bool) @@ -198,7 +317,7 @@ Exception: $"/restore -restoreProperty:%s{restoreProperties}" let arguments = - $"%s{restoreArg} /t:%s{targets} %s{properties} --getItem:FscCommandLineArgs --getItem:ProjectReference --getItem:_MSBuildProjectReferenceExistent --getProperty:OutputType -warnAsMessage:NU1608" + $"%s{restoreArg} /t:%s{targets} %s{properties} --getItem:FscCommandLineArgs --getItem:ProjectReference --getItem:_MSBuildProjectReferenceExistent --getProperty:OutputType --getProperty:ProjectAssetsFile --getProperty:MSBuildProjectExtensionsPath -warnAsMessage:NU1608" let! json = dotnet_msbuild_with_defines options.RootDir fsproj.FullName arguments options.FableOptions.Define @@ -254,6 +373,19 @@ Exception: let outputType = properties.GetProperty("OutputType").GetString() + // fickl + restoreInputs[fsproj.FullName] <- + [ + properties.GetProperty("ProjectAssetsFile").GetString() + let extensionsPath = + properties.GetProperty("MSBuildProjectExtensionsPath").GetString() + + if not (String.IsNullOrWhiteSpace extensionsPath) then + for ext in [ ".nuget.g.props"; ".nuget.g.targets" ] do + Path.Combine(extensionsPath, fsproj.Name + ext) + ] + |> List.filter (String.IsNullOrWhiteSpace >> not) + return { ProjectOptions = options @@ -263,9 +395,230 @@ Exception: } } + /// fickl: the TargetFramework a project file states: one unconditional literal, and no TargetFrameworks. + let tryStatedTargetFramework (fsproj: FileInfo) = + try + let doc = System.Xml.Linq.XDocument.Load fsproj.FullName + + let named name = + doc.Root.Descendants() + |> Seq.filter (fun el -> el.Name.LocalName = name) + |> Seq.toList + + let isUnconditional (el: System.Xml.Linq.XElement) = + Seq.append [ el ] (el.Ancestors()) + |> Seq.forall (fun el -> isNull (el.Attribute(System.Xml.Linq.XName.Get "Condition"))) + + match named "TargetFramework", named "TargetFrameworks" with + | [ el ], [] when isUnconditional el -> + let value = el.Value.Trim() + + if value = "" || value.Contains("$(") || value.Contains(";") then + None + else + Some value + | _ -> None + with _ -> + None + + /// fickl: querying the TargetFramework is a `dotnet msbuild` process of its own ahead of the design-time build. When + /// the project file states it, the design-time build starts with that one while the query runs beside it, and runs + /// again with the queried one if they differ (e.g. a Directory.Build.targets setting it). + let mkOptionsFromDesignTimeBuild + (referenceTargetFrameworks: ConcurrentDictionary) + (isMain: bool) + (fsproj: FileInfo) + (options: CrackerOptions) + : Async + = + async { + match referenceTargetFrameworks.ContainsKey fsproj.FullName, tryStatedTargetFramework fsproj with + | true, _ + | _, None -> return! mkOptionsFromDesignTimeBuildAux referenceTargetFrameworks isMain fsproj options + | false, Some stated -> + let configurationProperty = + if String.IsNullOrWhiteSpace options.Configuration then + "" + else + $"/p:Configuration=%s{options.Configuration}" + + let! query = + queryTargetFramework fsproj options configurationProperty + |> Async.Catch + |> Async.StartChild + + referenceTargetFrameworks[fsproj.FullName] <- stated + + let! guessed = + mkOptionsFromDesignTimeBuildAux referenceTargetFrameworks isMain fsproj options + |> Async.Catch + + let! queried = query + + let! targetFramework = + match queried with + | Choice1Of2 targetFramework -> async.Return targetFramework + // The evaluation may have read a file the design-time build's restore was writing + | Choice2Of2 _ -> queryTargetFramework fsproj options configurationProperty + + match guessed with + | Choice1Of2 response when targetFramework = stated -> return response + | Choice2Of2 e when targetFramework = stated -> + Runtime.ExceptionServices.ExceptionDispatchInfo.Capture(e).Throw() + return Unchecked.defaultof<_> + | _ -> + Log.verbose ( + lazy $"%s{fsproj.Name} states TargetFramework %s{stated} but evaluates to %s{targetFramework}" + ) + + referenceTargetFrameworks[fsproj.FullName] <- targetFramework + return! mkOptionsFromDesignTimeBuildAux referenceTargetFrameworks isMain fsproj options + } + +/// fickl: references are cracked once the main project's design-time build has finished, level by level, each a +/// `dotnet msbuild` process of its own. The project references a project file names directly start their design-time +/// builds alongside the main one instead. A result is used when the build that references the project asks for the +/// same TargetFramework and the main build's restore wrote none of the restore outputs it read after it started. +module private SpeculativeReferences = + + type Speculation = + { + TargetFramework: string + Started: DateTime + ReferenceTargetFrameworks: ConcurrentDictionary + Result: Threading.Tasks.Task> + } + + let isEnabled () = + Environment.GetEnvironmentVariable "FICKL_CRACK_SPECULATION" <> "0" + + /// Existing project references the project file names unconditionally, without properties or wildcards. + let statedProjectReferences (fsproj: FileInfo) = + try + let doc = System.Xml.Linq.XDocument.Load fsproj.FullName + + let isUnconditional (el: System.Xml.Linq.XElement) = + Seq.append [ el ] (el.Ancestors()) + |> Seq.forall (fun el -> isNull (el.Attribute(System.Xml.Linq.XName.Get "Condition"))) + + doc.Root.Descendants() + |> Seq.filter (fun el -> el.Name.LocalName = "ProjectReference" && isUnconditional el) + |> Seq.choose (fun el -> el.Attribute(System.Xml.Linq.XName.Get "Include") |> Option.ofObj) + |> Seq.collect (fun att -> + att.Value.Split(';', StringSplitOptions.RemoveEmptyEntries ||| StringSplitOptions.TrimEntries) + ) + |> Seq.filter (fun path -> not (path.Contains("$(") || path.Contains("@(") || path.Contains("*"))) + |> Seq.map (fun path -> FileInfo(Path.Combine(fsproj.DirectoryName, path))) + |> Seq.filter _.Exists + |> Seq.toList + with _ -> + [] + + /// Starts the design-time builds of the stated reference closure, nearest first, at most one per core. + let start (options: CrackerOptions) (mainProj: FileInfo) = + let speculations = + ConcurrentDictionary(StringComparer.OrdinalIgnoreCase) + + let seen = + Collections.Generic.HashSet([ mainProj.FullName ], StringComparer.OrdinalIgnoreCase) + + let isExcluded (fsproj: FileInfo) = + let name = Path.GetFileNameWithoutExtension fsproj.Name + + options.Exclude + |> List.exists (fun e -> String.Equals(e, name, StringComparison.OrdinalIgnoreCase)) + + let rec visit (level: FileInfo list) = + let next = + level + |> List.collect statedProjectReferences + |> List.filter (fun fsproj -> not (isExcluded fsproj) && seen.Add fsproj.FullName) + + for fsproj in next do + if speculations.Count < Environment.ProcessorCount then + match MSBuildCrackerResolver.tryStatedTargetFramework fsproj with + | Some targetFramework -> + let targetFrameworks = ConcurrentDictionary() + targetFrameworks[fsproj.FullName] <- targetFramework + + speculations[fsproj.FullName] <- + { + TargetFramework = targetFramework + Started = DateTime.UtcNow + ReferenceTargetFrameworks = targetFrameworks + Result = + MSBuildCrackerResolver.mkOptionsFromDesignTimeBuildAux + targetFrameworks + false + fsproj + options + |> Async.Catch + |> Async.StartAsTask + } + | None -> () + + if not (List.isEmpty next) && speculations.Count < Environment.ProcessorCount then + visit next + + visit [ mainProj ] + speculations + + /// The speculative result for a reference, when the build that references it agrees with it. + let tryAccept + (referenceTargetFrameworks: ConcurrentDictionary) + (options: CrackerOptions) + (fsproj: FileInfo) + (speculation: Speculation) + = + match speculation.Result.GetAwaiter().GetResult() with + | Choice2Of2 e -> + Log.verbose (lazy $"Speculative design-time build of %s{fsproj.Name} failed: %s{e.Message}") + None + | Choice1Of2 response -> + let expected = + match referenceTargetFrameworks.TryGetValue fsproj.FullName with + | true, targetFramework -> Some targetFramework + | _ -> None + + let rewritten = + if options.NoRestore then + [] + else + match MSBuildCrackerResolver.restoreInputs.TryGetValue fsproj.FullName with + | true, files -> + files + |> List.filter (fun file -> + let info = FileInfo file + info.Exists && info.LastWriteTimeUtc >= speculation.Started + ) + | _ -> [ "(unknown)" ] + + if expected <> Some speculation.TargetFramework then + Log.verbose ( + lazy + $"Speculative design-time build of %s{fsproj.Name} used %s{speculation.TargetFramework}, the reference asks for %A{expected}" + ) + + None + elif not (List.isEmpty rewritten) then + Log.verbose (lazy $"Restore rewrote %A{rewritten} after the speculative build of %s{fsproj.Name}") + None + else + Log.verbose (lazy $"Using the speculative design-time build of %s{fsproj.Name}") + + for KeyValue(path, targetFramework) in speculation.ReferenceTargetFrameworks do + if not (String.Equals(path, fsproj.FullName, StringComparison.OrdinalIgnoreCase)) then + referenceTargetFrameworks[path] <- targetFramework + + Some response + type MSBuildCrackerResolver() = let referenceTargetFrameworks = ConcurrentDictionary() + // fickl + let mutable speculations = + ConcurrentDictionary(StringComparer.OrdinalIgnoreCase) + interface ProjectCrackerResolver with member _.GetProjectOptionsFromProjectFile(isMain, options: CrackerOptions, projectFile) = let fsproj = FileInfo projectFile @@ -273,9 +626,39 @@ type MSBuildCrackerResolver() = if not fsproj.Exists then invalidArg (nameof fsproj) $"\"%s{fsproj.FullName}\" does not exist." - // Bad I know... - let result = - MSBuildCrackerResolver.mkOptionsFromDesignTimeBuildAux referenceTargetFrameworks isMain fsproj options - |> Async.RunSynchronously + let crack () = + // Bad I know... + let result = + MSBuildCrackerResolver.mkOptionsFromDesignTimeBuild referenceTargetFrameworks isMain fsproj options + |> Async.RunSynchronously + + result + + // fickl: see SpeculativeReferences + if isMain then + speculations <- + if SpeculativeReferences.isEnabled () then + SpeculativeReferences.start options fsproj + else + ConcurrentDictionary(StringComparer.OrdinalIgnoreCase) - result + try + crack () + with e when not speculations.IsEmpty -> + // A speculative build may have held a file the main build's restore was replacing + Log.verbose ( + lazy $"Cracking %s{fsproj.Name} beside speculative builds failed, retrying: %s{e.Message}" + ) + + for speculation in speculations.Values do + speculation.Result.Wait() + + speculations.Clear() + crack () + else + match speculations.TryRemove fsproj.FullName with + | true, speculation -> + match SpeculativeReferences.tryAccept referenceTargetFrameworks options fsproj speculation with + | Some response -> response + | None -> crack () + | _ -> crack () diff --git a/src/Fable.Compiler/ProjectCracker.fs b/src/Fable.Compiler/ProjectCracker.fs index 9cf5b403ec..b6cc8871df 100644 --- a/src/Fable.Compiler/ProjectCracker.fs +++ b/src/Fable.Compiler/ProjectCracker.fs @@ -158,7 +158,12 @@ type CrackerOptions(cliArgs: CliArgs, evaluateOnly: bool) = pendingDllBuilds[normalizedDllPath] <- Task.Run(fun () -> - Process.runSyncWithOutput projDir "dotnet" [ "build"; "-c"; cliArgs.Configuration; "--no-restore" ] + // fickl: dotnetEnvVars + Process.runSyncWithOutputEnv + (Process.dotnetEnvVars ()) + projDir + "dotnet" + [ "build"; "-c"; cliArgs.Configuration; "--no-restore" ] ) member _.WaitForBuiltDlls() = diff --git a/src/Fable.Compiler/Util.fs b/src/Fable.Compiler/Util.fs index 9b938f5cbb..aefd1c769b 100644 --- a/src/Fable.Compiler/Util.fs +++ b/src/Fable.Compiler/Util.fs @@ -474,6 +474,22 @@ module Process = let runSync (workingDir: string) (exePath: string) (args: string list) = runSyncWithEnv [] workingDir exePath args + /// fickl: environment for the `dotnet` processes the compiler starts for itself (cracking, building excluded + /// projects), not for the user's --run commands. The .NET CLI sends telemetry when it exits and waits for it, + /// which on some networks takes seconds per process; it is turned off unless the user set + /// DOTNET_CLI_TELEMETRY_OPTOUT. + let dotnetEnvVars () = + if isNull (Environment.GetEnvironmentVariable "DOTNET_CLI_TELEMETRY_OPTOUT") then + [ "DOTNET_CLI_TELEMETRY_OPTOUT", "1" ] + else + [] + + /// fickl: runSyncWithOutput with environment variables + let runSyncWithOutputEnv envVars workingDir exePath args = + let p = startProcess true envVars workingDir exePath args + p.WaitForExit 7000 |> ignore + p.StandardOutput.ReadToEnd() + let runSyncWithOutput workingDir exePath args = let p = startProcess true [] workingDir exePath args // Don't wait indefinitely to run process diff --git a/src/Fable.Compiler/Util.fsi b/src/Fable.Compiler/Util.fsi index f856957270..c34c8548f0 100644 --- a/src/Fable.Compiler/Util.fsi +++ b/src/Fable.Compiler/Util.fsi @@ -93,6 +93,10 @@ module Process = val runSync: workingDir: string -> exePath: string -> args: string list -> int val runSyncWithOutput: string -> string -> string list -> string + /// fickl: environment for the `dotnet` processes the compiler starts for itself + val dotnetEnvVars: unit -> (string * string) list + /// fickl: runSyncWithOutput with environment variables + val runSyncWithOutputEnv: envVars: (string * string) list -> string -> string -> string list -> string type PathResolver = abstract TryPrecompiledOutPath: sourceDir: string * relativePath: string -> string option diff --git a/src/Fable.Transforms/Babel/Fable2Babel.fs b/src/Fable.Transforms/Babel/Fable2Babel.fs index f3a630f375..672d5fd65a 100644 --- a/src/Fable.Transforms/Babel/Fable2Babel.fs +++ b/src/Fable.Transforms/Babel/Fable2Babel.fs @@ -917,16 +917,17 @@ module Annotation = AbstractMember.abstractProperty (prop, tagType, isComputed = isComputed) - match uci.UnionCaseFields with - // | [ field ] when field.Name = "Item" -> - // IntersectionTypeAnnotation - // [| - // field.FieldType |> resolveInlineType genArgs |> makeFieldAnnotation com ctx - // ObjectTypeAnnotation [| tagMember |] - // |] - | fields -> + match tryInlinedInterface com ent uci with + | Ok(Some fieldType) -> + IntersectionTypeAnnotation + [| + fieldType |> resolveInlineType genArgs |> makeFieldAnnotation com ctx + ObjectTypeAnnotation [| tagMember |] + |] + | Ok None + | Error _ -> let names, types = - fields |> List.map (fun fi -> fi.Name, fi.FieldType) |> List.unzip + uci.UnionCaseFields |> List.map (fun fi -> fi.Name, fi.FieldType) |> List.unzip makeAnonymousRecordTypeAnnotation com ctx (List.toArray names) types |> function diff --git a/src/Fable.Transforms/Directory.Build.props b/src/Fable.Transforms/Directory.Build.props index b566c47ee0..58a5fcd999 100644 --- a/src/Fable.Transforms/Directory.Build.props +++ b/src/Fable.Transforms/Directory.Build.props @@ -3,7 +3,8 @@ true - netstandard2.0 + + net10.0 $(OtherFlags) --nowarn:3536 embedded diff --git a/src/Fable.Transforms/FSharp2Fable.Util.fs b/src/Fable.Transforms/FSharp2Fable.Util.fs index 33613729e6..5cbecb5fd5 100644 --- a/src/Fable.Transforms/FSharp2Fable.Util.fs +++ b/src/Fable.Transforms/FSharp2Fable.Util.fs @@ -140,8 +140,8 @@ type FsUnionCase(uci: FSharpUnionCase) = /// The macro of a CaseTestAttribute, an extra test for a TypeScriptTaggedUnion case static member CaseTest(uci: FSharpUnionCase) = - uci.Attributes - |> Helpers.tryFindAttrib Atts.caseTest + [ Atts.caseTest; Atts.ficklCaseTest ] + |> List.tryPick (fun fullName -> Helpers.tryFindAttrib fullName uci.Attributes) |> Option.map (fun (att: FSharpAttribute) -> att.ConstructorArguments[0] |> snd |> string) // static member HasNamedFields(uci: FSharpUnionCase) = @@ -154,6 +154,7 @@ type FsUnionCase(uci: FSharpUnionCase) = member _.CompiledName = FsUnionCase.CompiledName uci member _.UnionCaseFields = uci.Fields |> Seq.mapToList (fun x -> upcast FsField(x)) + member _.Attributes = uci.Attributes |> Seq.map (fun x -> FsAtt(x) :> Fable.Attribute) type FsAtt(att: FSharpAttribute) = interface Fable.Attribute with @@ -402,7 +403,12 @@ type FsEnt(maybeAbbrevEnt: FSharpEntity) = // When compiling with netcoreapp target, netstandard only contains redirects // We can find the actual assembly name from the entity qualified name | "netstandard" -> ent.QualifiedName.Split(',').[1].Trim() |> Fable.CoreAssemblyName - | Naming.fablePrecompile -> + // fickl: the package cache names its further assemblies Fable.Precompiled., as assemblies loaded + // together need distinct names + | dllName when + dllName = Naming.fablePrecompile + || dllName.StartsWith(Naming.fablePrecompile + ".", StringComparison.Ordinal) + -> let sourcePath = FsEnt.SourcePath ent Fable.PrecompiledLib(sourcePath, Path.normalizePath asmPath) | dllName when Compiler.CoreAssemblyNames.Contains(dllName) -> Fable.CoreAssemblyName dllName diff --git a/src/Fable.Transforms/FSharp2Fable.fs b/src/Fable.Transforms/FSharp2Fable.fs index f14860ef00..3d4296be77 100644 --- a/src/Fable.Transforms/FSharp2Fable.fs +++ b/src/Fable.Transforms/FSharp2Fable.fs @@ -95,6 +95,14 @@ let private transformStringEnumCase (com: Compiler) (rule: Fable.Core.CaseRules) | Beam -> transformStringEnumAsAtom rule unionCase | _ -> transformStringEnum rule unionCase +/// The interface type a TypeScriptTaggedUnion case compiles as; reports an error when InlineInterface cannot apply. +let private inlinedInterface (com: IFableCompiler) (ctx: Context) r tdef (unionCase: FSharpUnionCase) = + match tryInlinedInterface com (FsEnt tdef) (FsUnionCase unionCase) with + | Ok inlined -> inlined + | Error msg -> + addError com ctx.InlinePath r msg + None + let private transformNewUnion com ctx r fsType (unionCase: FSharpUnionCase) (argExprs: Fable.Expr list) = match getUnionPattern fsType unionCase with | ErasedUnionCase -> makeTuple r false argExprs @@ -109,31 +117,50 @@ let private transformNewUnion com ctx r fsType (unionCase: FSharpUnionCase) (arg |> addErrorAndReturnNull com ctx.InlinePath r | argExprs -> makeTuple r false argExprs - | TypeScriptTaggedUnion(_, _, tagName, rule) -> - match argExprs with - // | [ argExpr ] when not (FsUnionCase.HasNamedFields unionCase) -> argExpr - | _ -> - let isCompiledValue, tagExpr = - match FsUnionCase.CompiledValue unionCase with - | None -> false, transformStringEnum rule unionCase - | Some(CompiledValue.Integer i) -> false, makeIntConst i - | Some(CompiledValue.Float f) -> false, makeFloatConst f - | Some(CompiledValue.Boolean b) -> false, makeBoolConst b - - match isCompiledValue, com.Options.Language with - | true, TypeScript -> - "CompileValue attribute is not supported in TypeScript" - |> addErrorAndReturnNull com ctx.InlinePath r - | _ -> - let fieldNames, fieldTypes = - unionCase.Fields - |> Seq.map (fun fi -> fi.Name, fi.FieldType) - |> Seq.toArray - |> Array.unzip + | TypeScriptTaggedUnion(tdef, _, tagName, rule) -> + let inlined = inlinedInterface com ctx r tdef unionCase - let fieldTypes = makeTypeGenArgs ctx.GenericArgs fieldTypes + let isCompiledValue, tagExpr = + match FsUnionCase.CompiledValue unionCase with + | None -> false, transformStringEnum rule unionCase + | Some(CompiledValue.Integer i) -> false, makeIntConst i + | Some(CompiledValue.Float f) -> false, makeFloatConst f + | Some(CompiledValue.Boolean b) -> false, makeBoolConst b - let expr = + match isCompiledValue, com.Options.Language with + | true, TypeScript -> + "CompileValue attribute is not supported in TypeScript" + |> addErrorAndReturnNull com ctx.InlinePath r + | _ -> + let fieldNames, fieldTypes = + unionCase.Fields + |> Seq.map (fun fi -> fi.Name, fi.FieldType) + |> Seq.toArray + |> Array.unzip + + let fieldTypes = makeTypeGenArgs ctx.GenericArgs fieldTypes + + let expr = + match inlined, argExprs with + | Some _, [ argExpr ] -> + // A copy of the interface value, with its prototype, carrying the tag + let tag = + Fable.NewAnonymousRecord([ tagExpr ], [| tagName |], [ tagExpr.Type ], false) + |> makeValue r + + let makeCopy argExpr = + emitExpr + r + Fable.Any + [ argExpr; tag ] + "Object.assign(Object.create(Object.getPrototypeOf($0)), $0, $1)" + + if canHaveSideEffects com argExpr then + let ident = getIdentUniqueName com ctx "caseValue" |> makeTypedIdent argExpr.Type + Fable.Let(ident, argExpr, makeCopy (Fable.IdentExpr ident)) + else + makeCopy argExpr + | _ -> Fable.NewAnonymousRecord( tagExpr :: argExprs, Array.append [| tagName |] fieldNames, @@ -142,7 +169,7 @@ let private transformNewUnion com ctx r fsType (unionCase: FSharpUnionCase) (arg ) |> makeValue r - Fable.TypeCast(expr, makeType ctx.GenericArgs fsType) + Fable.TypeCast(expr, makeType ctx.GenericArgs fsType) | StringEnum(tdef, rule) -> match argExprs with @@ -1358,17 +1385,17 @@ let private transformExpr (com: IFableCompiler) (ctx: Context) appliedGenArgs fs let index = unionCase.Fields |> Seq.findIndex (fun x -> x.Name = field.Name) return Fable.Get(unionExpr, Fable.TupleIndex index, fieldType, r) - | TypeScriptTaggedUnion _ -> - // if not (FsUnionCase.HasNamedFields unionCase) then - // return unionExpr - // else - let kind = - Fable.FieldInfo.Create( - FsField.FSharpFieldName field, - fieldType = makeType Map.empty field.FieldType - ) + | TypeScriptTaggedUnion(tdef, _, _, _) -> + match inlinedInterface com ctx r tdef unionCase with + | Some _ -> return Fable.TypeCast(unionExpr, fieldType) + | None -> + let kind = + Fable.FieldInfo.Create( + FsField.FSharpFieldName field, + fieldType = makeType Map.empty field.FieldType + ) - return Fable.Get(unionExpr, kind, fieldType, r) + return Fable.Get(unionExpr, kind, fieldType, r) | StringEnum _ -> return "StringEnum types cannot have fields" diff --git a/src/Fable.Transforms/State.fs b/src/Fable.Transforms/State.fs index 496ddd55cf..d07c710d99 100644 --- a/src/Fable.Transforms/State.fs +++ b/src/Fable.Transforms/State.fs @@ -339,6 +339,10 @@ type PrecompiledInfo = abstract TryGetRootModule: normalizedFullPath: string -> string option abstract TryGetInlineExpr: memberUniqueName: string -> InlineExpr option +// fickl: precompiled info whose declarations are spread over several assemblies (the package cache) lists them all +type PrecompiledAssemblies = + abstract DllPaths: string list + let private removeAll keys map = (map, keys) ||> Seq.fold (fun map key -> Map.remove key map) @@ -573,7 +577,13 @@ type CompilerImpl |> Option.bind (fun file -> ReadOnlyDictionary.tryFind entRef.FullName file.Entities) |> Option.orElseWith (fun () -> // Check also the precompiled dll because this may come from a precompiled inline expr - project.Assemblies.TryGetEntityByAssemblyPath(project.PrecompiledInfo.DllPath, entRef.FullName) + match project.PrecompiledInfo with + | :? PrecompiledAssemblies as info -> + info.DllPaths + |> List.tryPick (fun dllPath -> + project.Assemblies.TryGetEntityByAssemblyPath(dllPath, entRef.FullName) + ) + | info -> project.Assemblies.TryGetEntityByAssemblyPath(info.DllPath, entRef.FullName) ) member this.GetInlineExpr(memberUniqueName) = diff --git a/src/Fable.Transforms/Transforms.Util.fs b/src/Fable.Transforms/Transforms.Util.fs index bbe89065d1..227a2e5ac2 100644 --- a/src/Fable.Transforms/Transforms.Util.fs +++ b/src/Fable.Transforms/Transforms.Util.fs @@ -37,6 +37,9 @@ module Atts = [] let caseTest = "Fable.Core.CaseTestAttribute" // typeof.FullName + [] + let ficklCaseTest = "Fickl.Core.CaseTestAttribute" // typeof.FullName + [] let entryPoint = "Microsoft.FSharp.Core.EntryPointAttribute" // typeof.FullName @@ -103,6 +106,12 @@ module Atts = [] let tsTaggedUnion = "Fable.Core.TypeScriptTaggedUnionAttribute" // typeof.FullName + [] + let inlineInterface = "Fable.Core.InlineInterfaceAttribute" // typeof.FullName + + [] + let ficklInlineInterface = "Fickl.Core.InlineInterfaceAttribute" // typeof.FullName + [] let stringEnum = "Fable.Core.StringEnumAttribute" // typeof.FullName @@ -929,6 +938,31 @@ module AST = | _ -> ValueNone | _ -> ValueNone + /// The interface type a TypeScriptTaggedUnion case compiles as, when InlineInterface applies to the case. + /// An error carries the message for a case InlineInterface is applied to but cannot inline. + let tryInlinedInterface (com: Compiler) (ent: Entity) (uci: UnionCase) : Result = + let hasAtt (atts: Attribute seq) = + atts + |> Seq.exists (fun att -> + att.Entity.FullName = Atts.inlineInterface + || att.Entity.FullName = Atts.ficklInlineInterface + ) + + let onCase = hasAtt uci.Attributes + + let isInterface = + function + | DeclaredType(entRef, _) -> com.TryGetEntity(entRef) |> Option.exists (fun ent -> ent.IsInterface) + | _ -> false + + match uci.UnionCaseFields with + | _ when not (onCase || hasAtt ent.Attributes) -> Ok None + | [] when not onCase -> Ok None + | [ field ] when isInterface field.FieldType -> Ok(Some field.FieldType) + | _ -> + Error + $"InlineInterface cannot apply to '%s{uci.Name}': the case must have exactly one field, whose type is an interface" + // TODO: Improve this, see https://github.com/fable-compiler/Fable/issues/1659#issuecomment-445071965 // This is mainly used for inlining so a computation or a reference to a mutable value are understood // as a side effects too (because we don't want to duplicate or change the order of execution) diff --git a/src/Fickl.Branding.targets b/src/Fickl.Branding.targets new file mode 100644 index 0000000000..310cb61d82 --- /dev/null +++ b/src/Fickl.Branding.targets @@ -0,0 +1,111 @@ + + + + 0.5.3 + https://github.com/shayanhabibi/fickl + https://github.com/shayanhabibi/fickl.git + fsharp;fickl;fable;javascript;typescript;python;f#;js + Alfonso García-Caro Núñez;Fable contributors;Fickl contributors + + + + Fickl + fickl + Fickl: a performance-focused fork of the Fable F# compiler + + true + $(FicklVersion) + + + + + $(FicklVersion) + + + + Fickl.Core + + + + + + $(FicklVersion) + + + + + + + false + + + + win-x64;win-arm64;linux-x64;linux-arm64;linux-musl-x64;osx-x64;osx-arm64;any + $(RuntimeIdentifiers) + false + + + + true + + + + + false + + + + + + true + + + + + + + $(IntermediateOutputPath)Fickl.Version.fs + + + + <_FicklCompile Include="@(Compile)" /> + + + + + + + + Fickl.Compiler + + + + Fickl.AST + + + + Fickl.PublishUtils + + diff --git a/src/Fickl.Build/Fickl.Build.fsproj b/src/Fickl.Build/Fickl.Build.fsproj new file mode 100644 index 0000000000..b86c0711af --- /dev/null +++ b/src/Fickl.Build/Fickl.Build.fsproj @@ -0,0 +1,24 @@ + + + Exe + net10.0 + Major + false + + + + + + + + + + + + + + + + + + diff --git a/src/Fickl.Build/Fickl.fs b/src/Fickl.Build/Fickl.fs new file mode 100644 index 0000000000..ddd3dedb04 --- /dev/null +++ b/src/Fickl.Build/Fickl.fs @@ -0,0 +1,143 @@ +/// Fickl's own build commands. +module Fickl.Build.Fickl + +open System.IO +open Partas.Build +open Fickl.Build.Forwarded + +let private cliProject = Path.Combine(Paths.root, "src", "Fable.Cli") + +let private cliDll = + Path.Combine(cliProject, "bin", "Release", "net10.0", "fable.dll") + +let private buildCli = + stage "build compiler" { run (cmd $"dotnet build {cliProject} -c Release") } + +let private fcsForkUrl = "https://github.com/shayanhabibi/fickle-fsharp.git" + +/// The files Fable references from lib/fcs, as produced by the FCS build. +let private fcsFiles = + [ + "FSharp.Compiler.Service.dll" + "FSharp.Compiler.Service.xml" + "FSharp.Compiler.Service.deps.json" + "FSharp.DependencyManager.Nuget.dll" + "FSharp.DependencyManager.Nuget.xml" + "FSharp.Core.dll" + "FSharp.Core.xml" + ] + +/// Builds the FCS fork and copies its binaries into lib/fcs, replacing upstream's. +let fcsFork = + let repo = + Input.option "--repo" + |> Input.description "Path to the fickle-fsharp checkout; cloned there if missing" + |> Input.def (Path.GetFullPath(Path.Combine(Paths.root, "..", "fickle-fsharp"))) + + let branch = + Input.option "--branch" + |> Input.description "Branch to clone when the checkout is missing" + |> Input.def "fickle_slim" + + let framework = + Input.option "--framework" + |> Input.description "The FCS target framework to copy from" + |> Input.def "net10.0" + + command "fcs-fork" { + description "Build the FCS fork (fickle-fsharp) and copy its binaries into lib/fcs" + + input { + let! repo = repo + and! branch = branch + and! framework = framework + + let repo = Path.GetFullPath repo + + let output = + Path.Combine(repo, "artifacts", "bin", "FSharp.Compiler.Service", "Release", framework) + + let libFcs = Path.Combine(Paths.root, "lib", "fcs") + + return + stage "fcs-fork" { + if not (Directory.Exists repo) then + stage "clone" { + run (cmd $"git clone --filter=blob:none --single-branch -b {branch} {fcsForkUrl} {repo}") + } + + stage "build" { + workingDir repo + // BUILDING_USING_DOTNET: outside the FCS solution the build otherwise expects the bootstrap compiler. + // FSHARPCORE_USE_PACKAGE=false: build FSharp.Core from the fork's source, as upstream's lib/fcs does. + run ( + cmd + $"dotnet build -c Release src/Compiler -p:BUILDING_USING_DOTNET=true -p:FSHARPCORE_USE_PACKAGE=false" + ) + } + + stage "copy to lib/fcs" { + run (fun _ -> + for file in fcsFiles do + File.Copy(Path.Combine(output, file), Path.Combine(libFcs, file), true) + printfn $"lib/fcs/{file}" + + 0 + ) + } + } + } + } + +/// Compiles a project with the Release compiler and prints its --timings report. +let timings = + let project = + Input.argument "project" + |> Input.description "The F# project or directory to compile" + |> Input.def "tests/Js/Main" + + let language = + Input.option "--lang" + |> Input.description "The target language" + |> Input.def "javascript" + |> Input.acceptOnlyFromAmong [ "javascript"; "typescript"; "python"; "dart"; "rust"; "php"; "beam" ] + + let sequential = + Input.option "--no-parallel-type-check" + |> Input.description "Type-check the whole project before Fable starts, so the phases do not overlap" + + let runs = + Input.option "--runs" + |> Input.description "How many times to compile, each in a fresh process" + |> Input.def 1 + |> Input.validate (fun runs -> + if runs >= 1 then + Ok() + else + Error "--runs must be at least 1" + ) + + command "timings" { + description "Compile a project with --timings to see where the time goes" + + input { + let! project = project + and! language = language + and! sequential = sequential + and! runs = runs + + return + stage "timings" { + buildCli + + for i in 1..runs do + stage $"compile (run {i})" { + run ( + cmd + $"dotnet {cliDll} {project} --lang {language} --outDir temp/timings/{language} --exclude Fable.Core --noCache --timings" + |> Cmd.argIf sequential [ "--noParallelTypeCheck" ] + ) + } + } + } + } diff --git a/src/Fickl.Build/Forwarded.fs b/src/Fickl.Build/Forwarded.fs new file mode 100644 index 0000000000..a46fa1f3d0 --- /dev/null +++ b/src/Fickl.Build/Forwarded.fs @@ -0,0 +1,204 @@ +/// +/// Upstream's build commands, declared as typed Partas.Build commands. +/// Each command declares the flags its Fable.Build handler reads, so they appear in --help, +/// and runs the handler in-process through Build.Main.main with the same arguments +/// upstream's ./build.sh would pass. Fable.Build itself stays untouched, so upstream merges stay cheap. +/// +module Fickl.Build.Forwarded + +open System.IO +open Partas.Build + +module Paths = + /// The repository root, which Fable.Build's relative paths resolve against. + let root = Path.GetFullPath(Path.Combine(__SOURCE_DIRECTORY__, "..", "..")) + +/// Boolean flags, each forwarded as-is when set. +let private flags (declared: (string * string) list) : InputSpec = + declared + |> List.map (fun (name, desc) -> + Input.option name + |> Input.description desc + |> InputSpec.ofInput + |> InputSpec.map (fun isSet -> + if isSet then + [ name ] + else + [] + ) + ) + |> InputSpec.sequence + |> InputSpec.map List.concat + +/// An option taking a value, forwarded as name value when given. +let private valued (name: string) (desc: string) : InputSpec = + Input.optionMaybe name + |> Input.description desc + |> InputSpec.ofInput + |> InputSpec.map ( + function + | Some value -> [ name; value ] + | None -> [] + ) + +let private fableBuild (args: string list) = + stage "fable-build" { run (fun _ -> Build.Main.main (Array.ofList args)) } + +/// A command running Fable.Build with fixed leading arguments followed by the forwarded options. +let private forward (name: string) (desc: string) (prefix: string list) (options: InputSpec) = + command name { + description desc + + input { + let! extra = options + return fableBuild (prefix @ extra) + } + } + +let private watch = "--watch", "Watch for changes and re-run" +let private forceLib = "--force-fable-library", "Force building fable-library" +let private noDotnet = "--no-dotnet", "In watch mode, do not run the .NET tests" +let private noMinify = "--no-minify", "Don't minify the JavaScript output" + +let private skipLibCore = + "--skip-fable-library-core", "Skip the slow Rust extension and .pyi rebuild" + +let private none = InputSpec.ret [] + +let fableLibrary = + let language = + Input.argument "language" + |> Input.description "The target to build fable-library for" + |> Input.acceptOnlyFromAmong [ "javascript"; "typescript"; "python"; "dart"; "rust"; "beam" ] + + command "fable-library" { + description "Build fable-library for a target" + + input { + let! language = language + return fableBuild [ "fable-library"; $"--{language}" ] + } + } + +let test = + let common = [ watch; forceLib; noDotnet ] + + command "test" { + description "Run the main test suites" + + addCommands + [ + forward + "javascript" + "Run the tests for JavaScript" + [ "test"; "javascript" ] + (flags + [ + yield! common + "--react-only", "Run only the tests for React" + "--standalone-only", "Run only the tests for the standalone version of Fable" + "--adaptive-only", "Run only the tests for the adaptive version of Fable" + "--temporal-only", + "Run only the main tests, compiled with the Temporal date/time representation" + ]) + forward "typescript" "Run the tests for TypeScript" [ "test"; "typescript" ] (flags common) + forward + "python" + "Run the tests for Python" + [ "test"; "python" ] + (flags + [ + yield! common + skipLibCore + "--type-check", "Type-check the generated code with Pyright" + "--format", "Format the generated code with the Ruff formatter" + "--compile-only", "Compile the tests without running them" + ]) + forward "dart" "Run the tests for Dart" [ "test"; "dart" ] (flags common) + forward + "rust" + "Run the tests for Rust" + [ "test"; "rust" ] + (InputSpec.map2 + (@) + (flags + [ + yield! common + "--ast-only", "Run only the tests for the AST" + "--no_std", "Legacy alias for --features no_std" + "--threaded", "Legacy alias for --features threaded" + ]) + (valued "--features" "Comma-separated features; omit or use \",\" for the defaults")) + forward "beam" "Run the tests for BEAM (Erlang)" [ "test"; "beam" ] (flags common) + forward "integration" "Run the integration test suite" [ "test"; "integration" ] none + forward + "standalone" + "Test the standalone version of Fable (Fable on Node.js)" + [ "test"; "standalone" ] + none + forward "compiler-js" "Test the JS-compiled Fable compiler" [ "test"; "compiler-js" ] none + forward + "plugins" + "Run the plugin test suite (MemberDeclarationPluginAttribute)" + [ "test"; "plugins" ] + none + ] + } + +let quicktest = + let common = [ watch; forceLib ] + + let lang name desc extra = + forward name desc [ "quicktest"; name ] (flags (common @ extra)) + + command "quicktest" { + description "Watch and re-run the quicktest project for one target" + + addCommands + [ + lang "javascript" "Run for JavaScript" [] + lang "typescript" "Run for TypeScript" [] + lang "python" "Run for Python" [ skipLibCore ] + lang "dart" "Run for Dart" [] + lang "rust" "Run for Rust" [] + lang "beam" "Run for BEAM (Erlang)" [] + ] + } + +let standalone = + forward + "standalone" + "Compile the standalone + worker version of Fable for Node.js" + [ "standalone" ] + (flags [ forceLib; noMinify; watch ]) + +let workerJs = + forward + "worker-js" + "Compile the worker for the standalone version of Fable" + [ "worker-js" ] + (flags [ forceLib; noMinify ]) + +let compilerJs = + forward + "compiler-js" + "Compile the Fable compiler to JavaScript" + [ "compiler-js" ] + (flags + [ + forceLib + noMinify + "--skip-fable-standalone", "Don't rebuild fable-standalone first" + ]) + +let package = + forward + "package" + "Generate local packages of the compiler and Fable.Core for testing in other projects" + [ "package" ] + (flags [ forceLib ]) + +/// Upstream's publish, github-release and automated-release push Fable's own package names, +/// so they are deliberately not exposed here. +let commands = + [ fableLibrary; test; quicktest; standalone; workerJs; compilerJs; package ] diff --git a/src/Fickl.Build/NuGet.config b/src/Fickl.Build/NuGet.config new file mode 100644 index 0000000000..21058dffeb --- /dev/null +++ b/src/Fickl.Build/NuGet.config @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/Fickl.Build/Program.fs b/src/Fickl.Build/Program.fs new file mode 100644 index 0000000000..ae93045550 --- /dev/null +++ b/src/Fickl.Build/Program.fs @@ -0,0 +1,17 @@ +module Fickl.Build.Program + +open System +open Partas.Build +open Fickl.Build.Forwarded + +[] +let main argv = + // Fable.Build resolves relative paths against the current directory + Environment.CurrentDirectory <- Paths.root + + rootCommand argv { + name "fickl-build" + description "Fickl build CLI. Run a command with --help for its options, or --explain to dry-run it" + workingDir Paths.root + addCommands [ yield! Forwarded.commands; Fickl.timings; Fickl.fcsFork; Release.publish ] + } diff --git a/src/Fickl.Build/Release.fs b/src/Fickl.Build/Release.fs new file mode 100644 index 0000000000..c4d4244631 --- /dev/null +++ b/src/Fickl.Build/Release.fs @@ -0,0 +1,181 @@ +/// Publishes the Fickl NuGet family together, with the tool pointer last. +module Fickl.Build.Release + +open System +open System.Diagnostics +open System.IO +open System.IO.Compression +open System.Xml.Linq +open Partas.Build +open Fickl.Build.Forwarded +open Build.FableLibrary + +let private runDotnet (arguments: string list) = + let info = + ProcessStartInfo("dotnet", UseShellExecute = false, WorkingDirectory = Paths.root) + + for argument in arguments do + info.ArgumentList.Add argument + // Do not echo arguments: nuget push includes the API key. + use child = Process.Start info + child.WaitForExit() + + if child.ExitCode <> 0 then + failwith $"dotnet {arguments.Head} failed with exit code {child.ExitCode}" + +let private version () = + XDocument.Load(Path.Combine(Paths.root, "src", "Fickl.Branding.targets")).Descendants(XName.Get "FicklVersion") + |> Seq.exactlyOne + |> _.Value + +let private projects = + [ + "Fable.AST" + "Fickl.Core" + "Fable.Compiler" + "Fable.PublishUtils" + "Fable.Cli" + ] + +let private packageIds = + [ + "Fickl.AST" + "Fickl.Core" + "Fickl.Compiler" + "Fickl.PublishUtils" + "Fickl.any" + "Fickl.win-x64" + "Fickl.win-arm64" + "Fickl.linux-x64" + "Fickl.linux-arm64" + "Fickl.linux-musl-x64" + "Fickl.osx-x64" + "Fickl.osx-arm64" + "Fickl" + ] + +let publish = + let packOnly = + Input.option "--pack-only" + |> Input.description "Build and validate packages without publishing" + + command "publish" { + description "Pack and publish all Fickl NuGet packages; reads FICKL_NUGET_KEY from the environment" + + input { + let! packOnly = packOnly + + return + stage "Fickl release" { + run (fun _ -> + let key = Environment.GetEnvironmentVariable "FICKL_NUGET_KEY" + + if not packOnly && String.IsNullOrWhiteSpace key then + failwith "Set FICKL_NUGET_KEY before publishing (or use --pack-only)" + + let releaseVersion = version () + let output = Path.Combine(Paths.root, "temp", "release-" + releaseVersion) + + // CLI payloads are generated files under temp; never rely on leftover build outputs. + BuildFableLibraryJavaScript().Run(true) + BuildFableLibraryTypeScript().Run(true) + BuildFableLibraryRust().Run(true) + BuildFableLibraryDart().Run(true) + BuildFableLibraryBeam().Run(true) + + for project in projects do + printfn $"Packing {project} {releaseVersion}" + + runDotnet + [ + "pack" + Path.Combine(Paths.root, "src", project) + "-c" + "Release" + "-o" + output + ] + // Validate the entire set before the first upload. Never publish upstream package IDs. + let packages = + packageIds + |> List.map (fun id -> + let path = Path.Combine(output, $"{id}.{releaseVersion}.nupkg") + use archive = ZipFile.OpenRead path + + let spec = + archive.Entries + |> Seq.filter (fun e -> e.FullName.EndsWith ".nuspec") + |> Seq.exactlyOne + + use stream = spec.Open() + let document = XDocument.Load stream + + let value name = + document.Descendants() |> Seq.find (fun e -> e.Name.LocalName = name) |> _.Value + + if value "id" <> id || value "version" <> releaseVersion then + failwith $"Unexpected package identity: {path}" + + for dependency in + document.Descendants() |> Seq.filter (fun e -> e.Name.LocalName = "dependency") do + let dependencyId = dependency.Attribute(XName.Get "id").Value + let dependencyVersion = dependency.Attribute(XName.Get "version").Value + + if + dependencyId.StartsWith("Fickl.", StringComparison.Ordinal) + && dependencyVersion <> releaseVersion + then + failwith $"Mismatched dependency {dependencyId} {dependencyVersion} in {path}" + + if + id = "Fickl.any" + || id.StartsWith("Fickl.win-", StringComparison.Ordinal) + || id.StartsWith("Fickl.linux-", StringComparison.Ordinal) + || id.StartsWith("Fickl.osx-", StringComparison.Ordinal) + then + for language in [ "js"; "ts"; "rust"; "dart"; "beam"; "php" ] do + let prefix = $"fable-library-{language}/" + + if + not ( + archive.Entries + |> Seq.exists (fun e -> + e.FullName.StartsWith(prefix, StringComparison.Ordinal) + && e.Length > 0L + ) + ) + then + failwith $"Missing {language} runtime library in {path}" + + path + ) + + if not packOnly then + for path in packages do + printfn $"Publishing {Path.GetFileName path}" + + runDotnet + [ + "nuget" + "push" + path + "--source" + "https://api.nuget.org/v3/index.json" + "--api-key" + key + "--skip-duplicate" + // Compiler uses embedded symbols, so its generated snupkg has no PDBs. + if + Path + .GetFileName(path) + .StartsWith("Fickl.Compiler.", StringComparison.Ordinal) + then + "--no-symbols" + ] + + printfn $"Validated {packages.Length} packages in {output}" + 0 + ) + } + } + } diff --git a/src/Fickl.Core/Fickl.Core.Types.fs b/src/Fickl.Core/Fickl.Core.Types.fs new file mode 100644 index 0000000000..9c22873f2b --- /dev/null +++ b/src/Fickl.Core/Fickl.Core.Types.fs @@ -0,0 +1,40 @@ +namespace Fickl.Core + +open System + +/// +/// Adds a test to a case of a TypeScriptTaggedUnion, checked after the tag matches. +/// Use it when several cases share a tag value. +/// +/// +/// The test is a JS expression in Emit syntax, where $0 is the tested value: +/// +/// [<TypeScriptTaggedUnion("state")>] +/// type Terminal = +/// | [<CompiledName("done"); CaseTest("'value' in $0")>] Done of value: float +/// | [<CompiledName("done"); CaseTest("'error' in $0")>] DoneError of error: string +/// | Pending +/// +/// +type CaseTestAttribute(test: string) = + inherit Attribute() + +/// +/// Compiles a case of a TypeScriptTaggedUnion as its interface field with the tag added, +/// instead of as an object holding the field. +/// On the union type, it applies to every case that has fields. +/// +/// +/// The case must have exactly one field, whose type is an interface: +/// +/// [<TypeScriptTaggedUnion("type")>] +/// type Command = +/// | [<InlineInterface>] Take of Take +/// | Save +/// +/// The TypeScript type of the Take case is Take & { type: "take" }. +/// Constructing the case copies the field value with its prototype and sets the tag on the copy. +/// Reading the field returns the union value. +/// +type InlineInterfaceAttribute() = + inherit Attribute() diff --git a/src/Fickl.Core/Fickl.Core.fsproj b/src/Fickl.Core/Fickl.Core.fsproj new file mode 100644 index 0000000000..8ec49f78e8 --- /dev/null +++ b/src/Fickl.Core/Fickl.Core.fsproj @@ -0,0 +1,24 @@ + + + + false + Fickl Core Library: Fickl-only attributes and Fickl.Core aliases over Fable.Core + netstandard2.0 + true + true + 0.1.0 + + + + + + + + + + + + + + + diff --git a/src/fable-library-ts/Date.ts b/src/fable-library-ts/Date.ts index c585623b14..c916c7d290 100644 --- a/src/fable-library-ts/Date.ts +++ b/src/fable-library-ts/Date.ts @@ -403,7 +403,9 @@ function dateToISOStringWithOffset(dateWithOffset: Date, offset: number) { } function dateToStringWithOffset(date: IDateTimeOffset, format?: string) { - const d = new Date(date.getTime() + (date.offset ?? 0)); + // fickl: the shifted date holds the offset's wall-clock time in its UTC fields, so it must be + // a Utc DateTime; a plain Date makes day/month/year/hour read the machine's local timezone + const d = DateTime(date.getTime() + (date.offset ?? 0), DateTimeKind.Utc); if (typeof format !== "string") { return d.toISOString().replace(/\.\d+/, "").replace(/[A-Z]|\.\d+/g, " ") + dateOffsetToString((date.offset ?? 0)); } else if (format.length === 1) { diff --git a/tests/Integration/Compiler/CompilerHelpersTests.fs b/tests/Integration/Compiler/CompilerHelpersTests.fs index 5b4bd5dfa7..ff9701e726 100644 --- a/tests/Integration/Compiler/CompilerHelpersTests.fs +++ b/tests/Integration/Compiler/CompilerHelpersTests.fs @@ -7,6 +7,10 @@ open Fable.Tests.Compiler.Util.Compiler let tests = testList "Compiler Helpers" [ + testCase "Fable 5 plugins can resolve their AST assembly reference" <| fun _ -> + let assembly = System.Reflection.Assembly.Load "Fable.AST, Version=5.1.0.0, Culture=neutral, PublicKeyToken=null" + assembly.GetType("Fable.AST.Fable.Expr", throwOnError = true) |> equal typeof + testCase "expectedVersionMatchesActual works for same major version" <| fun _ -> Fable.CompilerExt.expectedVersionMatchesActual "5.0.0" "5.0.0" |> equal true Fable.CompilerExt.expectedVersionMatchesActual "5.0.0" "5.0.1" |> equal true diff --git a/tests/Integration/Compiler/CompilerMessagesTests.fs b/tests/Integration/Compiler/CompilerMessagesTests.fs index 8a876ea99d..d1f5ac1717 100644 --- a/tests/Integration/Compiler/CompilerMessagesTests.fs +++ b/tests/Integration/Compiler/CompilerMessagesTests.fs @@ -311,6 +311,43 @@ type Terminal = | [] DoneError of error: string let isError (t: Terminal) = match t with DoneError _ -> true | Done _ -> false +""" + compile source + |> Assert.Is.strictSuccess + |> ignore + + testCase "InlineInterface on a case without an interface field emits error" <| fun _ -> + let source = + """ +open Fable.Core +open Fickl.Core + +[] +type Value = + | [] Count of int + | Empty + +let make () = Count 1 +""" + compile source + |> Assert.Exists.errorWith "InlineInterface cannot apply to 'Count'" + |> ignore + + testCase "InlineInterface on a case with an interface field does not emit error" <| fun _ -> + let source = + """ +open Fable.Core +open Fickl.Core + +type Take = + abstract fromIndex: int + +[] +type Command = + | Take of Take + | Save + +let fromIndex (c: Command) = match c with Take t -> t.fromIndex | Save -> 0 """ compile source |> Assert.Is.strictSuccess diff --git a/tests/Integration/Compiler/Fable.Tests.Compiler.fsproj b/tests/Integration/Compiler/Fable.Tests.Compiler.fsproj index 8a2b09954d..4999412a62 100644 --- a/tests/Integration/Compiler/Fable.Tests.Compiler.fsproj +++ b/tests/Integration/Compiler/Fable.Tests.Compiler.fsproj @@ -24,6 +24,7 @@ + diff --git a/tests/Integration/Compiler/IwsamWarningTests.fs b/tests/Integration/Compiler/IwsamWarningTests.fs new file mode 100644 index 0000000000..007a8085b3 --- /dev/null +++ b/tests/Integration/Compiler/IwsamWarningTests.fs @@ -0,0 +1,93 @@ +module Fable.Tests.Compiler.IwsamWarnings + +open System +open System.IO +open Expecto +open FSharp.Compiler.CodeAnalysis +open FSharp.Compiler.Diagnostics +open FSharp.Compiler.SourceCodeServices + +let private check name source = + let file = Path.Combine(__SOURCE_DIRECTORY__, name + ".fs") + let references = + (AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES") :?> string).Split(Path.PathSeparator) + |> Array.map (fun path -> "-r:" + path) + + let options: FSharpProjectOptions = + { + ProjectFileName = file + "proj" + ProjectId = None + SourceFiles = [| file |] + OtherOptions = + Array.append + [| "--target:library"; "--targetprofile:netcore"; "--noframework"; "--langversion:preview" |] + references + ReferencedProjects = [||] + IsIncompleteTypeCheckEnvironment = false + UseScriptResolutionRules = false + LoadTime = DateTime.MaxValue + UnresolvedReferences = None + OriginalLoadReferences = [] + Stamp = None + } + + let checker = InteractiveChecker.Create(options) + checker.ParseAndCheckProject(options.ProjectFileName, options.SourceFiles, fun _ -> 0, lazy source) + |> Async.RunSynchronously + |> fun result -> result.Diagnostics + +let private cases = [ + "ordinary inherited generic interface", 0, """module Test +type I<'T> = abstract M: 'T -> 'T +type J<'T> = inherit I<'T> +let f (x: J) = x +"""; + "static abstract warns at each use", 2, """module Test +#nowarn "3535" +type I<'T> = static abstract M: 'T -> 'T +let f (x: I) = x +let g (x: I) = x +"""; + "inherited static abstract", 1, """module Test +#nowarn "3535" +type I<'T> = static abstract M: 'T -> 'T +type J<'T> = inherit I<'T> +let f (x: J) = x +"""; + "concrete static member", 0, """module Test +[] +type I = + abstract M: unit -> int + static member Create() = 1 +let f (x: I) = x +"""; + "recursive interface declarations", 2, """module rec Test +#nowarn "3535" +type I = abstract M: J -> unit +and J = static abstract Create: unit -> J +let f (x: J) = x +"""; + "imported static abstract interface", 1, """module Test +let f (x: System.IParsable) = x +"""; + "concrete static member hides inherited slot", 0, """module Test +#nowarn "3535" +type I = static abstract M: unit -> int +[] +type J = + inherit I + static member M() = 1 +let f (x: J) = x +"""; +] + +let tests = + testList "IWSAM warnings" [ + for name, expectedCount, source in cases do + testCase name <| fun _ -> + let diagnostics = check name source + let errors = diagnostics |> Array.filter (fun d -> d.Severity = FSharpDiagnosticSeverity.Error) + Expect.isEmpty errors "The interface declarations should type-check" + let warnings = diagnostics |> Array.filter (fun d -> d.ErrorNumber = 3536) + Expect.equal warnings.Length expectedCount "Preserve static-abstract interface usage warnings" + ] diff --git a/tests/Integration/Compiler/Main.fs b/tests/Integration/Compiler/Main.fs index d8fddd671f..a0904cb35c 100644 --- a/tests/Integration/Compiler/Main.fs +++ b/tests/Integration/Compiler/Main.fs @@ -7,6 +7,7 @@ let allTests = [ CompilerMessages.tests CodeServicesApi.tests + IwsamWarnings.tests AnonRecordInInterface.tests CompilerHelpers.tests Inflate.tests diff --git a/tests/Integration/Compiler/TestProject/TestProject.fsproj b/tests/Integration/Compiler/TestProject/TestProject.fsproj index 80a9298133..380706d232 100644 --- a/tests/Integration/Compiler/TestProject/TestProject.fsproj +++ b/tests/Integration/Compiler/TestProject/TestProject.fsproj @@ -13,6 +13,7 @@ + diff --git a/tests/Js/Main/DateTimeOffsetTests.fs b/tests/Js/Main/DateTimeOffsetTests.fs index c52e7557ab..8113190880 100644 --- a/tests/Js/Main/DateTimeOffsetTests.fs +++ b/tests/Js/Main/DateTimeOffsetTests.fs @@ -112,6 +112,24 @@ let tests = DateTimeOffset(2014, 9, 1, 16, 37, 2, TimeSpan.Zero).ToString("s", CultureInfo.InvariantCulture) |> equal "2014-09-01T16:37:02" + testCase "DateTimeOffset.ToString uses the offset's date in every local timezone" <| fun _ -> + // At +14 and -12 the offset's date differs from the UTC date, so one of the two + // differs from the date in any local timezone + let early = DateTimeOffset(2014, 9, 1, 0, 30, 5, TimeSpan.FromHours 14) + let late = DateTimeOffset(2014, 9, 1, 23, 30, 5, TimeSpan.FromHours -12) + for d in [ early; late ] do + let format (f: string) = d.ToString(f, CultureInfo.InvariantCulture) + let time = if d = early then "00:30" else "23:30" + format "D" |> equal "Monday, 01 September 2014" + format "d" |> equal "09/01/2014" + format "F" |> equal ("Monday, 01 September 2014 " + time + ":05") + format "f" |> equal ("Monday, 01 September 2014 " + time) + format "G" |> equal ("09/01/2014 " + time + ":05") + format "g" |> equal ("09/01/2014 " + time) + format "M" |> equal "September 01" + format "Y" |> equal "2014 September" + format "dd/MM/yyyy HH:mm" |> equal ("01/09/2014 " + time) + testCase "DateTimeOffset from Year 1 to 99 works" <| fun () -> let date = DateTimeOffset(1, 1, 2, 0, 0, 0, TimeSpan.Zero) date.Year |> equal 1 diff --git a/tests/Js/Main/Fable.Tests.fsproj b/tests/Js/Main/Fable.Tests.fsproj index ce2d68b963..24e73c8d99 100644 --- a/tests/Js/Main/Fable.Tests.fsproj +++ b/tests/Js/Main/Fable.Tests.fsproj @@ -19,6 +19,7 @@ + diff --git a/tests/Js/Main/JsInteropTests.fs b/tests/Js/Main/JsInteropTests.fs index d8a914b13f..17d92a84a4 100644 --- a/tests/Js/Main/JsInteropTests.fs +++ b/tests/Js/Main/JsInteropTests.fs @@ -3,6 +3,7 @@ module Fable.Tests.JsInterop open System open Util.Testing open Fable.Core +open Fickl.Core #if FABLE_COMPILER open Fable.Core.JsInterop @@ -398,6 +399,30 @@ module TaggedUnion = | [] DoneFailed of ok: bool | Pending + type Take = + abstract fromIndex: int + abstract toIndex: int + + type Edit = + abstract text: string + + type TakeRange(fromIndex: int, toIndex: int) = + interface Take with + member _.fromIndex = fromIndex + member _.toIndex = toIndex + + [] + type InlinedCase = + | [] Take of Take + | Edit of Edit + | Save + + [] + type InlinedType = + | Take of Take + | Edit of Edit + | Save + #if FABLE_COMPILER module PojoDefinedByConsArgs = [] @@ -1119,6 +1144,59 @@ let tests = TaggedUnion.SharedTag.DoneError "oops" (next ()).IsDoneError |> equal true calls |> equal 1 + + testCase "InlineInterface on a TS tagged union case compiles the case as its interface" <| fun () -> + let take = { new TaggedUnion.Take with + member _.fromIndex = 1 + member _.toIndex = 3 } + let command = TaggedUnion.InlinedCase.Take take + command?``type`` |> equal "take" + command?fromIndex |> equal 1 + jsTypeof command?Item |> equal "undefined" + jsTypeof take?``type`` |> equal "undefined" + let edit = TaggedUnion.InlinedCase.Edit { new TaggedUnion.Edit with member _.text = "hi" } + edit?Item?text |> equal "hi" + let describe = function + | TaggedUnion.InlinedCase.Take t -> sprintf "take: %d-%d" t.fromIndex t.toIndex + | TaggedUnion.InlinedCase.Edit e -> sprintf "edit: %s" e.text + | TaggedUnion.InlinedCase.Save -> "save" + command |> describe |> equal "take: 1-3" + edit |> describe |> equal "edit: hi" + TaggedUnion.InlinedCase.Save |> describe |> equal "save" + !!{| ``type`` = "take"; fromIndex = 2; toIndex = 5 |} |> describe |> equal "take: 2-5" + command.IsTake |> equal true + edit.IsTake |> equal false + + testCase "InlineInterface on a TS tagged union type applies to every case with fields" <| fun () -> + let describe = function + | TaggedUnion.InlinedType.Take t -> sprintf "take: %d-%d" t.fromIndex t.toIndex + | TaggedUnion.InlinedType.Edit e -> sprintf "edit: %s" e.text + | TaggedUnion.InlinedType.Save -> "save" + let edit = TaggedUnion.InlinedType.Edit { new TaggedUnion.Edit with member _.text = "hi" } + edit?``type`` |> equal "edit" + edit?text |> equal "hi" + edit |> describe |> equal "edit: hi" + TaggedUnion.InlinedType.Save |> describe |> equal "save" + !!{| ``type`` = "edit"; text = "raw" |} |> describe |> equal "edit: raw" + + testCase "InlineInterface keeps the prototype of a class instance" <| fun () -> + let range = TaggedUnion.TakeRange(1, 3) + let command = TaggedUnion.InlinedType.Take range + match command with + | TaggedUnion.InlinedType.Take t -> sprintf "%d-%d" t.fromIndex t.toIndex |> equal "1-3" + | _ -> failwith "expected Take" + jsTypeof range?``type`` |> equal "undefined" + + testCase "InlineInterface evaluates the case value once" <| fun () -> + let mutable calls = 0 + let next () = + calls <- calls + 1 + { new TaggedUnion.Take with + member _.fromIndex = 1 + member _.toIndex = 3 } + let command = TaggedUnion.InlinedCase.Take(next ()) + command.IsTake |> equal true + calls |> equal 1 #endif testCase "Pattern matching with StringEnum works" <| fun () ->