// Package parse reads pqc-bench .out files produced by the SLURM harness. // // Each file contains a SLURM prolog header followed by 1–N "loop spin" blocks. // Each spin block reports one median+average pair per benchmarked operation. package parse import ( "bufio" "fmt" "os" "strconv" "strings" ) // Meta holds the SLURM prolog metadata extracted from the file header. type Meta struct { JobID string JobName string Node string StartedAt string Directory string // Explicit fields emitted by submit.sh for reliable downstream parsing. BenchVariant string BenchParam string BenchNSpins string } // Measurement is a single operation's reported statistics for one loop spin. type Measurement struct { Median int64 Average int64 } // Run holds everything parsed from one .out file. type Run struct { File string Meta Meta // Spins[i] maps operation name → measurement for loop spin i+1. Spins []map[string]Measurement } // ParseFile reads a single .out file and returns a Run. func ParseFile(path string) (*Run, error) { f, err := os.Open(path) if err != nil { return nil, err } defer f.Close() run := &Run{File: path} scanner := bufio.NewScanner(f) // Default buffer size is 64KB; lines are short so this is fine. var currentSpin map[string]Measurement var currentOp string var pendingMedian int64 inSpin := false for scanner.Scan() { line := strings.TrimSpace(scanner.Text()) // SLURM prolog lines start with ## if strings.HasPrefix(line, "##") { parsePrologLine(line, &run.Meta) continue } // New loop spin if strings.HasPrefix(line, "Loop spin:") { if inSpin && currentSpin != nil { run.Spins = append(run.Spins, currentSpin) } currentSpin = make(map[string]Measurement) currentOp = "" inSpin = true continue } if !inSpin { continue } // Operation name line ends with ':' if strings.HasSuffix(line, ":") && !strings.HasPrefix(line, "median") && !strings.HasPrefix(line, "average") { currentOp = strings.TrimSuffix(line, ":") currentOp = strings.TrimSpace(currentOp) continue } if currentOp == "" { continue } if strings.HasPrefix(line, "median:") { v, err := parseCycles(line) if err != nil { return nil, fmt.Errorf("%s: %w", path, err) } pendingMedian = v continue } if strings.HasPrefix(line, "average:") { avg, err := parseCycles(line) if err != nil { return nil, fmt.Errorf("%s: %w", path, err) } currentSpin[currentOp] = Measurement{Median: pendingMedian, Average: avg} currentOp = "" pendingMedian = 0 continue } } if err := scanner.Err(); err != nil { return nil, fmt.Errorf("%s: %w", path, err) } // Flush last spin if inSpin && currentSpin != nil { run.Spins = append(run.Spins, currentSpin) } return run, nil } // parseCycles extracts the integer from lines like "median: 25194 cycles/ticks". func parseCycles(line string) (int64, error) { // Format: "