Files
nushell/scripts/parser-bench-throughput.nu
Darren Schroeder c123f61d7e feat(parser): add benchmarks for parser performance and throughput analysis (#18245)
<!--
Thanks for contributing to Nushell!

Before submitting, please read the contributing guide:
https://github.com/nushell/nushell/blob/main/CONTRIBUTING.md

This template helps reviewers understand your changes and allows us to
generate high-quality release notes.
-->

## Description
This PR adds some parser benchmarks. See how to run them below. You
should get an output similar this below. Hopefully my math is right.
```nushell
Throughput summary (derived from benchmark timing, unit: micro):
╭────┬─────────────────────────────────────────┬───────┬───────┬─────────┬───────────┬────────────────┬────────────────╮
│  # │                benchmark                │ chars │ bytes │ seconds │ time_unit │ chars_per_unit │ bytes_per_unit │
├────┼─────────────────────────────────────────┼───────┼───────┼─────────┼───────────┼────────────────┼────────────────┤
│  0 │ parser_lex_large_97332b_97098c          │ 97098 │ 97332 │    0.00 │ micro     │         246.38 │         246.97 │
│  1 │ parser_lex_medium_4315b_4315c           │  4315 │  4315 │    0.00 │ micro     │         169.22 │         169.22 │
│  2 │ parser_lex_small_82b_82c                │    82 │    82 │    0.00 │ micro     │         106.11 │         106.11 │
│  3 │ parser_lite_medium_4315b_4315c          │  4315 │  4315 │    0.00 │ micro     │          95.46 │          95.46 │
│  4 │ parser_lite_real_world_620b_620c        │   620 │   620 │    0.00 │ micro     │          38.51 │          38.51 │
│  5 │ parser_lite_small_82b_82c               │    82 │    82 │    0.00 │ micro     │           5.99 │           5.99 │
│  6 │ parser_parse_block_medium_4315b_4315c   │  4315 │  4315 │    0.00 │ micro     │          37.52 │          37.52 │
│  7 │ parser_parse_block_real_world_620b_620c │   620 │   620 │    0.00 │ micro     │          24.31 │          24.31 │
│  8 │ parser_parse_block_small_82b_82c        │    82 │    82 │    0.00 │ micro     │           4.77 │           4.77 │
│  9 │ parser_parse_large_97332b_97098c        │ 97098 │ 97332 │    0.00 │ micro     │          97.10 │          97.33 │
│ 10 │ parser_parse_medium_4315b_4315c         │  4315 │  4315 │    0.00 │ micro     │          44.62 │          44.62 │
│ 11 │ parser_parse_small_82b_82c              │    82 │    82 │    0.00 │ micro     │           4.27 │           4.27 │
╰────┴─────────────────────────────────────────┴───────┴───────┴─────────┴───────────┴────────────────┴────────────────╯


Headline metric candidates (chars/micro):
╭───┬──────────────────────────────────┬────────────────╮
│ # │            benchmark             │ chars_per_unit │
├───┼──────────────────────────────────┼────────────────┤
│ 0 │ parser_lex_large_97332b_97098c   │         246.38 │
│ 1 │ parser_lex_medium_4315b_4315c    │         169.22 │
│ 2 │ parser_lex_small_82b_82c         │         106.11 │
│ 3 │ parser_parse_large_97332b_97098c │          97.10 │
│ 4 │ parser_lite_medium_4315b_4315c   │          95.46 │
╰───┴──────────────────────────────────┴────────────────╯


Benchmark logfile: target/parser-bench/benchmarks-parser__-20260519-125213.log
parser_lex_small_82b_82c                            [ 758.0 ns ... 772.8 ns ... 829.7 ns ]  stddev:  14.8 ns
parser_lex_medium_4315b_4315c                       [  25.2 us ...  25.5 us ...  26.1 us ]  stddev: 248.3 ns
parser_lex_large_97332b_97098c                      [ 389.0 us ... 394.1 us ... 410.0 us ]  stddev:   4.7 us
parser_lite_small_82b_82c                           [  13.2 us ...  13.7 us ...  17.7 us ]  stddev: 670.6 ns
parser_lite_medium_4315b_4315c                      [  45.0 us ...  45.2 us ...  46.0 us ]  stddev: 183.7 ns
parser_lite_real_world_620b_620c                    [  15.9 us ...  16.1 us ...  18.1 us ]  stddev: 374.2 ns
parser_parse_block_small_82b_82c                    [  16.7 us ...  17.2 us ...  19.0 us ]  stddev: 402.0 ns
parser_parse_block_medium_4315b_4315c               [ 111.0 us ... 115.0 us ... 124.3 us ]  stddev:   3.1 us
parser_parse_block_real_world_620b_620c             [  25.0 us ...  25.5 us ...  26.6 us ]  stddev: 411.2 ns
parser_parse_small_82b_82c                          [  18.7 us ...  19.2 us ...  20.8 us ]  stddev: 508.7 ns
parser_parse_medium_4315b_4315c                     [  95.2 us ...  96.7 us ... 113.4 us ]  stddev:   3.2 us
parser_parse_large_97332b_97098c                    [   1.0 ms ...   1.0 ms ...   1.0 ms ]  stddev:   3.8 us

   Compiling nu-cmd-lang v0.112.3 (/Users/fdncred/src/nushell/crates/nu-cmd-lang)
   Compiling nu-test-support v0.112.3 (/Users/fdncred/src/nushell/crates/nu-test-support)
   Compiling nu v0.112.3 (/Users/fdncred/src/nushell)
    Finished `bench` profile [optimized] target(s) in 1m 12s
     Running benches/benchmarks.rs (target/release/deps/benchmarks-5a8a0256451b1c57)
```
<!--
Explain what this PR does and why.

This section is intentionally flexible:
- Describe the problem
- Explain your approach
- Include technical details if relevant

Good examples:
- "In this PR, I fixed..."
- "In this PR, I added support for..."
- "This change improves X by..."

Write as much or as little as needed for reviewers to understand your
changes.
-->

## User-facing changes (Release notes)
Run the new parser benchmarks like this:
```nushell
nu scripts/parser-bench-run.nu
```
The `parser-bench-run.nu` script has these arguments.
```nushell
    --filter: string = "parser_*"   # benchmark name glob filter (tango uses glob matching)
    --bench: string = "benchmarks"  # bench target name
    --out-dir: path = "target/parser-bench"  # directory where logs are written
```

Analyze the new parser benchmarks like this:
```nushell
nu scripts/parser-bench-throughput.nu target/parser-bench/benchmarks-parser__-20260519-125213.log
```
The `parser-bench-throughput.nu` script has these arguments
```nushell
    log_file?: path   # output file produced by parser-bench-0.112.2-run.nu
    --name-prefix: string = "parser_" # benchmark name glob filter
    --time-unit: string = "micro"  # throughput time base: sec|milli|micro|nano
```
<!--
This section is used (mostly as-is) for https://www.nushell.sh/blog/

Describe how Nushell behavior changes from a user's perspective.
Do NOT describe internal Rust changes here.

Write in a release note style, for example:
- "Added support for..."
- "Fixed an issue where..."
- "Nushell now supports..."
- "Improved performance of..."

If your changes do NOT affect users (internal refactors, cleanup, etc.),
just write:
- "n/a"
- "nan"
- or similar

This tells us the change should not appear in the changelog.

Tips:
- Focus on observable behavior
- Include examples if helpful
- Keep it concise

You can:
- Write a short paragraph (will appear as a bullet point), OR
- Use headings (###) if your change needs more structure

Avoid writing things like:
- "In this PR, I refactored..."
- "This updates internal code..."

You may leave this blank until the PR is ready.
-->

## Additional notes
<!--
Optional.

Examples:
- fixes #123
- closes #456
- related #789

Anything else reviewers should know.
Remove this section if not needed.
-->
2026-05-19 15:58:47 -05:00

131 lines
4.1 KiB
Nu

#!/usr/bin/env nu
# Parse parser benchmark output and report throughput (chars/sec and bytes/sec).
# Compatible with Nushell 0.112.2.
def unit-to-seconds [value: float, unit: string] {
match $unit {
"ns" => { $value / 1_000_000_000.0 }
"us" => { $value / 1_000_000.0 }
"µs" => { $value / 1_000_000.0 }
"ms" => { $value / 1_000.0 }
"s" => { $value }
_ => { null }
}
}
def normalize-time-unit [unit: string] {
let normalized = ($unit | str downcase)
match $normalized {
"sec" | "s" | "second" | "seconds" => { "sec" }
"milli" | "ms" | "millisecond" | "milliseconds" => { "milli" }
"micro" | "us" | "µs" | "microsecond" | "microseconds" => { "micro" }
"nano" | "ns" | "nanosecond" | "nanoseconds" => { "nano" }
_ => { null }
}
}
def selected-unit-to-seconds [unit: string] {
match $unit {
"sec" => { 1.0 }
"milli" => { 0.001 }
"micro" => { 0.000001 }
"nano" => { 0.000000001 }
_ => { null }
}
}
def extract-sizes [name: string] {
let capture = ($name | parse -r '_(?<bytes>[0-9]+)b_(?<chars>[0-9]+)c$')
if ($capture | is-empty) {
null
} else {
{
bytes: ($capture.0.bytes | into int)
chars: ($capture.0.chars | into int)
}
}
}
def main [
log_file?: path # output file produced by parser-bench-0.112.2-run.nu
--name-prefix: string = "parser_"
--time-unit: string = "micro" # throughput time base: sec|milli|micro|nano
] {
if $log_file == null {
error make {
msg: "Missing required argument: log_file"
help: "Usage: parser-bench-throughput.nu <log-file>\n\nExample:\n ./target/debug/nu scripts/parser-bench-throughput.nu target/parser-bench/benchmarks-parser__-20260519-125213.log"
}
}
let normalized_time_unit = (normalize-time-unit $time_unit)
if $normalized_time_unit == null {
error make {
msg: $"unsupported time unit: ($time_unit)"
help: "supported values: sec, milli, micro, nano"
}
}
let selected_unit_seconds = (selected-unit-to-seconds $normalized_time_unit)
let rows = (
open $log_file
| lines
| parse -r '^(?<name>parser_[A-Za-z0-9_]+)\s+\[.*?\.\.\.\s*(?<value>[0-9]+(?:\.[0-9]+)?)\s*(?<unit>ns|us|µs|ms|s)\s*\.\.\.'
| where {|row| $row.name | str starts-with $name_prefix }
| each {|row|
let seconds = (unit-to-seconds ($row.value | into float) $row.unit)
let sizes = (extract-sizes $row.name)
let unit_time = if $seconds == null {
null
} else {
$seconds / $selected_unit_seconds
}
if ($seconds == null or $sizes == null or $unit_time == null or $unit_time == 0.0) {
null
} else {
{
benchmark: $row.name
seconds: ($seconds | into float)
chars: $sizes.chars
bytes: $sizes.bytes
time_unit: $normalized_time_unit
chars_per_unit: (($sizes.chars | into float) / $unit_time)
bytes_per_unit: (($sizes.bytes | into float) / $unit_time)
}
}
}
| compact
| uniq-by benchmark
| sort-by benchmark
)
if ($rows | is-empty) {
error make {
msg: "no parser benchmark rows were parsed"
help: "ensure the log contains parser benchmark output lines with timing units"
}
}
print $"Throughput summary \(derived from benchmark timing, unit: ($normalized_time_unit)\):"
print ($rows
| select benchmark chars bytes seconds time_unit chars_per_unit bytes_per_unit
| table)
print $"\nHeadline metric candidates \(chars/($normalized_time_unit)\):"
print ($rows
| sort-by chars_per_unit --reverse
| first 5
| select benchmark chars_per_unit
| table)
# print "\nFull table of all parsed benchmarks with throughput metrics:"
# print $rows
print $"\nBenchmark logfile: ($log_file)"
open $log_file
}