-
Notifications
You must be signed in to change notification settings - Fork 39
Add runtime benchmark baseline before Msg redesign #1023
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
emil14
wants to merge
14
commits into
main
Choose a base branch
from
codex/runtime-benchmarks-baseline
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 6 commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
06c983b
Add runtime benchmark baseline for Msg redesign
emil14 ffb79b0
Address PR feedback on runtime benchmark design
emil14 f57fc48
Expand runtime e2e benchmarks across data type paths
emil14 0920abd
Document benchmark intent in all e2e Neva programs
emil14 77479d4
benchmarks: expand runtime e2e baseline suite
emil14 0937089
Merge branch 'main' into codex/runtime-benchmarks-baseline
emil14 5b48a53
Merge branch 'main' into codex/runtime-benchmarks-baseline
emil14 2af5ff4
benchmarks: align runtime baseline with current main
emil14 895e532
Merge branch 'main' into codex/runtime-benchmarks-baseline
emil14 357e366
test(e2e): annotate fixture copy helpers for gosec
emil14 122f24e
test(e2e): align fixture helper suppressions with gosec
emil14 32d2379
Merge origin/main into codex/runtime-benchmarks-baseline
emil14 102e048
Merge remote-tracking branch 'origin/main' into codex/runtime-benchma…
emil14 29ecfec
Merge remote-tracking branch 'origin/main' into codex/runtime-benchma…
emil14 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,38 +1,181 @@ | ||
| // Remember - Go runs benchmark function twice: | ||
emil14 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| // first time for calibration, second time for actual benchmark. | ||
| // This might affect working with relative paths! | ||
|
|
||
| package test | ||
emil14 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| import ( | ||
| "context" | ||
| "fmt" | ||
| "io/fs" | ||
| "os" | ||
| "os/exec" | ||
| "path/filepath" | ||
| "sort" | ||
| "strings" | ||
| "testing" | ||
| "time" | ||
|
|
||
| "github.com/stretchr/testify/require" | ||
| "github.com/nevalang/neva/pkg/e2e" | ||
| ) | ||
|
|
||
| func BenchmarkMessagePassing(b *testing.B) { | ||
| // Store original working directory | ||
| originalWd, err := os.Getwd() | ||
| require.NoError(b, err) | ||
| // BenchmarkRuntimeE2E benchmarks precompiled runtime programs by data type path. | ||
| func BenchmarkRuntimeE2E(b *testing.B) { | ||
| // Build the CLI once and reuse it for all benchmark programs. | ||
| repoRoot := e2e.FindRepoRoot(b) | ||
| nevaBin := e2e.BuildNevaBinary(b, repoRoot) | ||
|
|
||
| benchPkgs, err := discoverRuntimeBenchPkgs(repoRoot) | ||
| if err != nil { | ||
| b.Fatalf("discover runtime benchmark packages: %v", err) | ||
| } | ||
|
|
||
| for _, benchPkg := range benchPkgs { | ||
| benchName := strings.ReplaceAll(benchPkg, string(filepath.Separator), "_") | ||
| b.Run(benchName, func(b *testing.B) { | ||
| // Build the benchmark program once outside timed iterations. | ||
| progPath := buildProgramOnce(b, repoRoot, nevaBin, benchPkg) | ||
|
|
||
| b.ReportAllocs() | ||
| b.ResetTimer() | ||
|
|
||
| // Change to parent directory | ||
| err = os.Chdir("..") | ||
| require.NoError(b, err) | ||
| for b.Loop() { | ||
| runProgramBinary(b, progPath) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| // discoverRuntimeBenchPkgs finds all benchmark packages under benchmarks/runtime_bench. | ||
| func discoverRuntimeBenchPkgs(repoRoot string) ([]string, error) { | ||
| benchmarksRoot := filepath.Join(repoRoot, "benchmarks") | ||
| runtimeRoot := filepath.Join(benchmarksRoot, "runtime_bench") | ||
| pkgs := make([]string, 0, 64) | ||
|
|
||
| // Ensure we return to original directory after benchmark | ||
| defer func() { | ||
| err := os.Chdir(originalWd) | ||
| require.NoError(b, err) | ||
| }() | ||
| walkErr := filepath.WalkDir(runtimeRoot, func(path string, d fs.DirEntry, err error) error { | ||
| if err != nil { | ||
| return err | ||
| } | ||
| if d.IsDir() || d.Name() != "main.neva" { | ||
| return nil | ||
| } | ||
|
|
||
| pkgDir := filepath.Dir(path) | ||
| relDir, relErr := filepath.Rel(benchmarksRoot, pkgDir) | ||
| if relErr != nil { | ||
| return fmt.Errorf("resolve relative package dir for %q: %w", path, relErr) | ||
| } | ||
| pkgs = append(pkgs, relDir) | ||
| return nil | ||
| }) | ||
| if walkErr != nil { | ||
| return nil, walkErr | ||
| } | ||
|
|
||
| sort.Strings(pkgs) | ||
| return pkgs, nil | ||
| } | ||
|
|
||
| // buildProgramOnce prepares an isolated module and compiles one benchmark package. | ||
| func buildProgramOnce(b *testing.B, repoRoot, nevaBin, pkgName string) string { | ||
| b.Helper() | ||
|
|
||
| // Create an isolated temp module workspace for one benchmark package. | ||
| tmpDir := b.TempDir() | ||
| homeDir := filepath.Join(tmpDir, "home") | ||
| moduleDir := filepath.Join(tmpDir, "bench-module") | ||
emil14 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| progDir := filepath.Join(moduleDir, pkgName) | ||
| if err := os.MkdirAll(progDir, 0o755); err != nil { | ||
| b.Fatalf("create benchmark module dirs: %v", err) | ||
| } | ||
| if err := prepareBenchmarkHome(repoRoot, homeDir); err != nil { | ||
| b.Fatalf("prepare benchmark home: %v", err) | ||
| } | ||
|
|
||
| // Copy benchmark fixture files into the isolated module. | ||
| copyFile(b, filepath.Join(repoRoot, "benchmarks", "neva.yml"), filepath.Join(moduleDir, "neva.yml")) | ||
| copyFile( | ||
| b, | ||
| filepath.Join(repoRoot, "benchmarks", pkgName, "main.neva"), | ||
| filepath.Join(progDir, "main.neva"), | ||
| ) | ||
|
|
||
| // Compile the benchmark program once and return its output binary. | ||
| buildProg := exec.Command(nevaBin, "build", pkgName) | ||
| buildProg.Dir = moduleDir | ||
| buildProg.Env = append(os.Environ(), "HOME="+homeDir) | ||
| if output, err := buildProg.CombinedOutput(); err != nil { | ||
| b.Fatalf("build benchmark program: %v\n%s", err, output) | ||
| } | ||
|
|
||
| // Reset timer after setup | ||
| b.ResetTimer() | ||
| return filepath.Join(moduleDir, "output") | ||
| } | ||
|
|
||
| // prepareBenchmarkHome creates an isolated Neva home with local stdlib available. | ||
| func prepareBenchmarkHome(repoRoot, homeDir string) error { | ||
| nevaHome := filepath.Join(homeDir, "neva") | ||
| if err := os.MkdirAll(nevaHome, 0o755); err != nil { | ||
| return err | ||
| } | ||
|
|
||
| stdSrc := filepath.Join(repoRoot, "std") | ||
| stdDst := filepath.Join(nevaHome, "std") | ||
| if err := os.Symlink(stdSrc, stdDst); err == nil { | ||
| return nil | ||
| } | ||
|
|
||
| return copyDir(stdSrc, stdDst) | ||
| } | ||
|
|
||
| // copyDir copies a directory recursively, preserving file modes. | ||
| func copyDir(src, dst string) error { | ||
emil14 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| return filepath.WalkDir(src, func(path string, d fs.DirEntry, err error) error { | ||
| if err != nil { | ||
| return err | ||
| } | ||
| rel, relErr := filepath.Rel(src, path) | ||
| if relErr != nil { | ||
| return relErr | ||
| } | ||
| target := filepath.Join(dst, rel) | ||
| if d.IsDir() { | ||
| return os.MkdirAll(target, 0o755) | ||
| } | ||
|
|
||
| data, readErr := os.ReadFile(path) | ||
| if readErr != nil { | ||
| return readErr | ||
| } | ||
| // #nosec G306 -- benchmark fixture files are read-only test assets. | ||
| return os.WriteFile(target, data, 0o644) | ||
| }) | ||
| } | ||
|
|
||
| // runProgramBinary executes one precompiled benchmark binary. | ||
| func runProgramBinary(b *testing.B, progPath string) { | ||
| b.Helper() | ||
|
|
||
| // #nosec G204 -- benchmark executes a fixed local binary built during setup. | ||
| ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) | ||
| defer cancel() | ||
|
|
||
| cmd := exec.CommandContext(ctx, progPath) | ||
| output, err := cmd.CombinedOutput() | ||
| if err != nil { | ||
| if ctx.Err() == context.DeadlineExceeded { | ||
| b.Fatalf("run benchmark program timed out: %s", progPath) | ||
| } | ||
| b.Fatalf("run benchmark program: %v\n%s", err, output) | ||
| } | ||
| } | ||
|
|
||
| // copyFile copies one fixture file into the temp benchmark module. | ||
| func copyFile(b *testing.B, src, dst string) { | ||
| b.Helper() | ||
|
|
||
| data, err := os.ReadFile(src) | ||
| if err != nil { | ||
| b.Fatalf("read %s: %v", src, err) | ||
| } | ||
|
|
||
| for b.Loop() { | ||
| cmd := exec.Command("neva", "run", "message_passing") | ||
| out, err := cmd.CombinedOutput() | ||
| require.NoError(b, err, string(out)) | ||
| // #nosec G306 -- benchmark fixture file should be readable for local runs/inspection. | ||
| if err := os.WriteFile(dst, data, 0o644); err != nil { | ||
| b.Fatalf("write %s: %v", dst, err) | ||
| } | ||
| } | ||
66 changes: 66 additions & 0 deletions
66
benchmarks/runtime_bench/complex/control_flow/selectors_routers/main.neva
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| // Benchmarks a more complex mixed control-flow workload. | ||
| // Combines selectors, routers, and union wrapping/unwrapping. | ||
| import { | ||
| runtime | ||
| streams | ||
| } | ||
|
|
||
| type Number union { | ||
| Int int | ||
| } | ||
|
|
||
| def Main(start any) (stop any) { | ||
| range streams.Range | ||
| map_complex streams.Map<int, int>{ComplexFlow} | ||
| wait streams.Wait | ||
| --- | ||
| :start -> [ | ||
| 1 -> range:from, | ||
| 10000 -> range:to | ||
| ] | ||
| range -> map_complex -> wait -> :stop | ||
| } | ||
|
|
||
| def ComplexFlow(data int) (res int) { | ||
| mod Mod | ||
| eq Eq<int> | ||
| tern Ternary<int> | ||
| match Match<int> | ||
| cond Cond<int> | ||
| switch Switch<int> | ||
| select Select<int> | ||
| race Race<int> | ||
| box Union<Number> | ||
| unwrap Switch<Number> | ||
| del Del | ||
| panic runtime.Panic | ||
| --- | ||
| :data -> [ | ||
| mod:left, | ||
| 2 -> mod:right, | ||
| 10 -> tern:then, | ||
| 20 -> tern:else, | ||
| 10 -> match:if[0], | ||
| 20 -> match:if[1], | ||
| 1 -> match:then[0], | ||
| 2 -> match:then[1], | ||
| 1 -> match:else, | ||
| 1 -> switch:case[0], | ||
| 2 -> switch:case[1], | ||
| 1000 -> select:then[0], | ||
| 2000 -> select:then[1], | ||
| 3000 -> select:then[2] | ||
| ] | ||
| mod -> [eq:left, 0 -> eq:right] | ||
| eq -> [tern:if, cond:if] | ||
| tern -> match:data | ||
| match -> [cond:data, switch:data] | ||
| [cond:then, cond:else] -> race:data | ||
| switch:case[0] -> [race:case[0], select:if[0]] | ||
| switch:case[1] -> [race:case[1], select:if[1]] | ||
| switch:else -> select:if[2] | ||
| select -> del | ||
| [race:case[0], race:case[1]] -> [box:data, Number::Int -> box:tag] | ||
| box -> [unwrap:data, Number::Int -> unwrap:case[0] -> :res] | ||
| unwrap:else -> panic | ||
| } |
54 changes: 54 additions & 0 deletions
54
benchmarks/runtime_bench/complex/types/struct_union_combo/main.neva
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| // Benchmarks a composite data path: struct + union together. | ||
| // This models realistic mixed-type runtime traffic. | ||
| import { | ||
| runtime | ||
| streams | ||
| } | ||
|
|
||
| type Number union { | ||
| Int int | ||
| } | ||
|
|
||
| type Envelope struct { | ||
| id int | ||
| boxed Number | ||
| } | ||
|
|
||
| def Main(start any) (stop any) { | ||
| range streams.Range | ||
| map_pack streams.Map<int, Envelope>{Pack} | ||
| map_unpack streams.Map<Envelope, int>{Unpack} | ||
| wait streams.Wait | ||
| --- | ||
| :start -> [ | ||
| 1 -> range:from, | ||
| 100000 -> range:to | ||
| ] | ||
| range -> map_pack -> map_unpack -> wait -> :stop | ||
| } | ||
|
|
||
| // Pack creates an envelope that contains both plain and tagged fields. | ||
| def Pack(data int) (res Envelope) { | ||
| box Union<Number> | ||
| builder Struct<Envelope> | ||
| --- | ||
| :data -> [ | ||
| box:data, | ||
| Number::Int -> box:tag, | ||
| builder:id | ||
| ] | ||
| box -> builder:boxed | ||
| builder -> :res | ||
| } | ||
|
|
||
| // Unpack extracts tagged payload from nested struct field. | ||
| def Unpack(data Envelope) (res int) { | ||
| switch Switch<Number> | ||
| panic runtime.Panic | ||
| --- | ||
| :data -> .boxed -> [ | ||
| switch:data, | ||
| Number::Int -> switch:case[0] -> :res | ||
| ] | ||
| switch:else -> panic | ||
| } |
24 changes: 24 additions & 0 deletions
24
benchmarks/runtime_bench/simple/collections/basic/main.neva
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| // Benchmarks collection helpers from builtin package. | ||
| // Covers Len over list constants. | ||
| import { streams } | ||
|
|
||
| const nums list<int> = [1, 2, 3, 4, 5] | ||
|
|
||
| def Main(start any) (stop any) { | ||
| range streams.Range | ||
| map_col streams.Map<int, int>{CollectionOps} | ||
| wait streams.Wait | ||
| --- | ||
| :start -> [ | ||
| 1 -> range:from, | ||
| 100000 -> range:to | ||
| ] | ||
| range -> map_col -> wait -> :stop | ||
| } | ||
|
|
||
| def CollectionOps(data int) (res int) { | ||
| len_list Len | ||
| --- | ||
| :data -> $nums -> len_list:data | ||
| len_list -> :res | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.