Skip to content

Commit 6891a5b

Browse files
committed
bench: add performance samples and report
- Add minimal samples for no includes, #include, #ref, #include+#ref cases - Add samples/perf.md with detailed performance analysis - Simplify root readme.md to focus on value prop, dnx go usage examples, and unchanged re-run numbers only - Include the go.targets cache fix needed to demonstrate short-circuiting re-runs
1 parent 72f368d commit 6891a5b

11 files changed

Lines changed: 217 additions & 6 deletions

File tree

readme.md

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# go
1+
# go#
22

33
[![Version](https://img.shields.io/nuget/vpre/go.svg?color=royalblue)](https://www.nuget.org/packages/go)
44
[![Downloads](https://img.shields.io/nuget/dt/go.svg?color=darkmagenta)](https://www.nuget.org/packages/go)
@@ -18,8 +18,54 @@ OSMF tier. A single fee covers all of [Devlooped packages](https://www.nuget.org
1818

1919
<!-- https://github.com/devlooped/.github/raw/main/osmf.md -->
2020
<!-- #content -->
21+
22+
## What is `go#`?
23+
24+
`go#` (go sharp) lets you run `.cs` files directly, like scripts, while still getting the full power of the .NET SDK (dependencies, compilation, AOT, etc.).
25+
26+
It shines for:
27+
- Quick one-off tools and prototypes
28+
- File-based apps without a `.csproj`
29+
- Fast iteration with smart caching (subsequent runs are near-instant when nothing changed)
30+
- Easy sharing of small utilities (just the `.cs` file)
31+
2132
## Usage
22-
*go*
33+
34+
```console
35+
# Run a file
36+
dnx go app.cs
37+
38+
# Pass arguments to your app
39+
dnx go app.cs -- arg1 arg2
40+
41+
# Pass arguments to the underlying `dotnet publish`
42+
dnx go app.cs /p:PublishAot=true -- arg1 arg2
43+
```
44+
45+
## Performance
46+
47+
The main advantage of `go#` is **fast unchanged re-runs**. The numbers below compare the three common approaches on pristine sources.
48+
49+
**PublishAot=false**
50+
51+
| Example | `dotnet run app.cs` | `go app.cs` | `dotnet publish ...; app.exe` |
52+
|--------------------|---------------------|--------------|-------------------------------|
53+
| a) no includes | 1.306s | 0.264s | 5.044s |
54+
| b) #include | 1.169s | 0.198s | 4.857s |
55+
| c) #ref | 4.605s | 0.191s | 6.243s |
56+
| d) #include + #ref | 13.079s | 0.227s | 7.320s |
57+
58+
**PublishAot=true** (NativeAOT)
59+
60+
| Example | `dotnet run app.cs` | `go app.cs` | `dotnet publish ...; app.exe` |
61+
|--------------------|---------------------|--------------|-------------------------------|
62+
| a) no includes || 0.227s | 12.436s |
63+
| b) #include || 0.164s | 11.145s |
64+
| c) #ref || 0.183s | 32.874s |
65+
| d) #include + #ref || 0.269s | 21.888s |
66+
67+
**Observation**: `go#` re-runs are fast (~0.16–0.27s) — just launching the cached executable when no changes are detected in any of the input files (including the `#include` and `#ref` dependencies).
68+
2369
<!-- #content -->
2470
---
2571
<!-- include https://github.com/devlooped/sponsors/raw/main/footer.md -->

samples/include/app.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
#!/usr/bin/env dotnet
2+
#:property TargetFramework=net10.0
3+
4+
#:include lib.cs
5+
6+
using PerfInclude;
7+
8+
Console.WriteLine("include: " + Helper.Message() + " (via #:include)");

samples/include/lib.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
namespace PerfInclude;
2+
3+
public static class Helper
4+
{
5+
public static string Message() => "included source merged";
6+
}

samples/incref/app.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
#!/usr/bin/env dotnet
2+
#:property TargetFramework=net10.0
3+
#:property ExperimentalFileBasedProgramEnableRefDirective=true
4+
5+
#:include inclib.cs
6+
#:ref reflib.cs
7+
8+
using PerfInc;
9+
using PerfRef;
10+
11+
Console.WriteLine("incref: " + Helper.Message() + " | " + Greeter.Greet("both") + " (via #:include + #:ref)");

samples/incref/inclib.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
namespace PerfInc;
2+
3+
public static class Helper
4+
{
5+
public static string Message() => "included+ref source";
6+
}

samples/incref/reflib.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
#:property TargetFramework=net10.0
2+
#:property OutputType=Library
3+
#:property ExperimentalFileBasedProgramEnableRefDirective=true
4+
5+
namespace PerfRef;
6+
7+
public static class Greeter
8+
{
9+
public static string Greet(string name) => $"hello from #:ref in combined, {name}";
10+
}

samples/noinc/app.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
#!/usr/bin/env dotnet
2+
#:property TargetFramework=net10.0
3+
4+
// a) no includes, no refs
5+
6+
Console.WriteLine("noinc: OK from file-based app (no directives beyond TF)");

samples/perf.md

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
# Performance Analysis (detailed)
2+
3+
This document contains the full performance measurements for reference. The main `readme.md` only highlights the key "unchanged re-run" numbers.
4+
5+
## Approaches
6+
7+
- `go app.cs` — invoked via the RID-specific published binary (never `dotnet run` on the tool project)
8+
- `dotnet run app.cs` — built-in file-based run (no AOT)
9+
- `dotnet publish app.cs /p:PublishAot=... ; artifacts/app/app.exe`
10+
11+
## Examples
12+
13+
Four minimal, package-free examples live under `samples/`:
14+
15+
- **a) no includes**`samples/noinc/app.cs`
16+
- **b) #include**`samples/include/{app.cs, lib.cs}`
17+
- **c) #ref**`samples/ref/{app.cs, lib.cs}`
18+
- **d) #include + #ref**`samples/incref/{app.cs, inclib.cs, reflib.cs}`
19+
20+
All declare `TargetFramework=net10.0` (plus the Experimental flag for #ref cases).
21+
22+
## Methodology
23+
24+
- Environment: Windows + .NET 11.0.100-preview.5 SDK.
25+
- All timings via `pwsh` `Measure-Command`.
26+
- `go` binary: always the RID-specific published exe (win-x64, --no-self-contained).
27+
- AOT control via `/p:PublishAot=false|true`.
28+
- Scenarios captured:
29+
- first-run (clean state)
30+
- unchanged re-runs (pristine sources)
31+
- run/edit/run
32+
- Raw data is in the goal scratch directory (`perf-logs/`).
33+
34+
The tool provides fast re-runs for unchanged sources through its build caching.
35+
36+
## Unchanged re-runs (pristine sources – last observed)
37+
38+
**PublishAot=false**
39+
40+
| Example | `dotnet run app.cs` | `go app.cs` (RID exe) | `dotnet publish ... ; app.exe` |
41+
|--------------------|---------------------|-----------------------|--------------------------------|
42+
| a) no includes | 1.306s | 0.264s | 5.044s |
43+
| b) #include | 1.169s | 0.198s | 4.857s |
44+
| c) #ref | 4.605s | 0.191s | 6.243s |
45+
| d) #include + #ref | 13.079s | 0.227s | 7.320s |
46+
47+
**PublishAot=true** (NativeAOT)
48+
49+
| Example | `dotnet run app.cs` | `go app.cs` (RID exe) | `dotnet publish ... ; app.exe` |
50+
|--------------------|---------------------|-----------------------|--------------------------------|
51+
| a) no includes || 0.227s | 12.436s |
52+
| b) #include || 0.164s | 11.145s |
53+
| c) #ref || 0.183s | 32.874s |
54+
| d) #include + #ref || 0.269s | 21.888s |
55+
56+
**Observation**: `go` re-runs (both AOT modes) are now ~0.16–0.27s — just launching the already-published executable.
57+
58+
## First-run (clean state)
59+
60+
AOT=true first runs are significantly slower due to NativeAOT compilation.
61+
62+
| Example | `dotnet run` | `go` (AOT=false) | `publish` (AOT=false) | `go` (AOT=true) | `publish` (AOT=true) |
63+
|---------|--------------|------------------|-----------------------|-----------------|----------------------|
64+
| a) noinc | 6–12s | ~20s | ~8–13s | ~20s | ~17–23s |
65+
| b) include | ~5–11s | ~20s | ~7s | ~18s | ~20s |
66+
| c) ref | ~6–7s | ~19s | ~8s | ~20s | ~18s |
67+
| d) incref | ~11s | ~38s | ~7.5s | ~32s | ~27s |
68+
69+
## run / edit / run
70+
71+
After editing the source, the next invocation was measured (with prior artifacts left in place).
72+
73+
See the raw logs for exact per-cell values. The important takeaway is that once a native AOT executable has been produced by `go`, launching it remains extremely fast even after source changes (until a rebuild is triggered).
74+
75+
## Full raw data
76+
77+
All `Measure-Command` output and per-run summaries live in the private scratch directory used during measurement (typically under the goal session temp folder).
78+
79+
Example commands used:
80+
81+
```pwsh
82+
& '...\go-tool-rid\go.exe' app.cs -- /p:PublishAot=false
83+
& '...\go-tool-rid\go.exe' app.cs -- /p:PublishAot=true
84+
```
85+
86+
---
87+
88+
*This file is for future reference. The primary user-facing numbers are in the root `readme.md`.*

samples/ref/app.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
#!/usr/bin/env dotnet
2+
#:property TargetFramework=net10.0
3+
#:property ExperimentalFileBasedProgramEnableRefDirective=true
4+
5+
#:ref lib.cs
6+
7+
using PerfRef;
8+
9+
Console.WriteLine("ref: " + Greeter.Greet("perf") + " (via #:ref)");

samples/ref/lib.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
#:property TargetFramework=net10.0
2+
#:property OutputType=Library
3+
#:property ExperimentalFileBasedProgramEnableRefDirective=true
4+
5+
namespace PerfRef;
6+
7+
public static class Greeter
8+
{
9+
public static string Greet(string name) => $"hello from separate #:ref assembly, {name}";
10+
}

0 commit comments

Comments
 (0)