@@ -4,15 +4,100 @@ package report
44// SPDX-License-Identifier: BSD-3-Clause
55
66import (
7+ "encoding/json"
78 "fmt"
89 "log/slog"
910 "perfspect/internal/script"
1011 "perfspect/internal/util"
11- "regexp"
1212 "strconv"
1313 "strings"
1414)
1515
16+ // fioOutput is the top-level struct for the FIO JSON report.
17+ // ref: https://fio.readthedocs.io/en/latest/fio_doc.html#json-output
18+ type fioOutput struct {
19+ FioVersion string `json:"fio version"`
20+ Timestamp int64 `json:"timestamp"`
21+ TimestampMs int64 `json:"timestamp_ms"`
22+ Time string `json:"time"`
23+ Jobs []fioJob `json:"jobs"`
24+ }
25+
26+ // Job represents a single job's results within the FIO report.
27+ type fioJob struct {
28+ Jobname string `json:"jobname"`
29+ Groupid int `json:"groupid"`
30+ JobStart int64 `json:"job_start"`
31+ Error int `json:"error"`
32+ Eta int `json:"eta"`
33+ Elapsed int `json:"elapsed"`
34+ Read fioIOStats `json:"read"`
35+ Write fioIOStats `json:"write"`
36+ Trim fioIOStats `json:"trim"`
37+ JobRuntime int `json:"job_runtime"`
38+ UsrCPU float64 `json:"usr_cpu"`
39+ SysCPU float64 `json:"sys_cpu"`
40+ Ctx int `json:"ctx"`
41+ Majf int `json:"majf"`
42+ Minf int `json:"minf"`
43+ IodepthLevel map [string ]float64 `json:"iodepth_level"`
44+ IodepthSubmit map [string ]float64 `json:"iodepth_submit"`
45+ IodepthComplete map [string ]float64 `json:"iodepth_complete"`
46+ LatencyNs map [string ]float64 `json:"latency_ns"`
47+ LatencyUs map [string ]float64 `json:"latency_us"`
48+ LatencyMs map [string ]float64 `json:"latency_ms"`
49+ LatencyDepth int `json:"latency_depth"`
50+ LatencyTarget int `json:"latency_target"`
51+ LatencyPercentile float64 `json:"latency_percentile"`
52+ LatencyWindow int `json:"latency_window"`
53+ }
54+
55+ // IOStats holds the detailed I/O statistics for read, write, or trim operations.
56+ type fioIOStats struct {
57+ IoBytes int64 `json:"io_bytes"`
58+ IoKbytes int64 `json:"io_kbytes"`
59+ BwBytes int64 `json:"bw_bytes"`
60+ Bw int64 `json:"bw"`
61+ Iops float64 `json:"iops"`
62+ Runtime int `json:"runtime"`
63+ TotalIos int `json:"total_ios"`
64+ ShortIos int `json:"short_ios"`
65+ DropIos int `json:"drop_ios"`
66+ SlatNs fioLatencyStats `json:"slat_ns"`
67+ ClatNs fioLatencyStatsPercentiles `json:"clat_ns"`
68+ LatNs fioLatencyStats `json:"lat_ns"`
69+ BwMin int `json:"bw_min"`
70+ BwMax int `json:"bw_max"`
71+ BwAgg float64 `json:"bw_agg"`
72+ BwMean float64 `json:"bw_mean"`
73+ BwDev float64 `json:"bw_dev"`
74+ BwSamples int `json:"bw_samples"`
75+ IopsMin int `json:"iops_min"`
76+ IopsMax int `json:"iops_max"`
77+ IopsMean float64 `json:"iops_mean"`
78+ IopsStddev float64 `json:"iops_stddev"`
79+ IopsSamples int `json:"iops_samples"`
80+ }
81+
82+ // fioLatencyStats holds basic latency metrics.
83+ type fioLatencyStats struct {
84+ Min int64 `json:"min"`
85+ Max int64 `json:"max"`
86+ Mean float64 `json:"mean"`
87+ Stddev float64 `json:"stddev"`
88+ N int `json:"N"`
89+ }
90+
91+ // LatencyStatsPercentiles holds latency metrics including percentiles.
92+ type fioLatencyStatsPercentiles struct {
93+ Min int64 `json:"min"`
94+ Max int64 `json:"max"`
95+ Mean float64 `json:"mean"`
96+ Stddev float64 `json:"stddev"`
97+ N int `json:"N"`
98+ Percentile map [string ]int64 `json:"percentile"`
99+ }
100+
16101func cpuSpeedFromOutput (outputs map [string ]script.ScriptOutput ) string {
17102 var vals []float64
18103 for line := range strings .SplitSeq (strings .TrimSpace (outputs [script .SpeedBenchmarkScriptName ].Stdout ), "\n " ) {
@@ -35,26 +120,40 @@ func cpuSpeedFromOutput(outputs map[string]script.ScriptOutput) string {
35120 return fmt .Sprintf ("%.0f" , util .GeoMean (vals ))
36121}
37122
38- func storagePerfFromOutput (outputs map [string ]script.ScriptOutput ) (readBW , writeBW string ) {
39- // fio output format:
40- // READ: bw=140MiB/s (146MB/s), 140MiB/s-140MiB/s (146MB/s-146MB/s), io=16.4GiB (17.6GB), run=120004-120004msec
41- // WRITE: bw=139MiB/s (146MB/s), 139MiB/s-139MiB/s (146MB/s-146MB/s), io=16.3GiB (17.5GB), run=120004-120004msec
42- re := regexp .MustCompile (` bw=(\d+[.]?[\d]*\w+\/s)` )
43- for line := range strings .SplitSeq (strings .TrimSpace (outputs [script .StorageBenchmarkScriptName ].Stdout ), "\n " ) {
44- if strings .Contains (line , "READ: bw=" ) {
45- matches := re .FindStringSubmatch (line )
46- if len (matches ) != 0 {
47- readBW = matches [1 ]
48- }
49- } else if strings .Contains (line , "WRITE: bw=" ) {
50- matches := re .FindStringSubmatch (line )
51- if len (matches ) != 0 {
52- writeBW = matches [1 ]
53- }
54- } else if strings .Contains (line , "ERROR: " ) {
55- slog .Error ("failed to run storage benchmark" , slog .String ("line" , line ))
56- }
123+ func storagePerfFromOutput (outputs map [string ]script.ScriptOutput ) (readLat , readBw , writeLat , writeBw string ) {
124+ output := outputs [script .StorageBenchmarkScriptName ].Stdout
125+ slog .Debug ("storage benchmark output" , slog .String ("output" , output ))
126+
127+ i := strings .Index (output , "{\n \" fio version\" " )
128+ if i >= 0 {
129+ output = output [i :]
130+ } else {
131+ slog .Error ("Unable to find fio output" , slog .String ("output" , output ))
132+ return
133+ }
134+ if strings .Contains (output , "ERROR:" ) {
135+ slog .Error ("failed to run storage benchmark" , slog .String ("output" , output ))
136+ return
57137 }
138+
139+ slog .Debug ("parsing storage benchmark output" )
140+ var fioData fioOutput
141+ if err := json .Unmarshal ([]byte (output ), & fioData ); err != nil {
142+ slog .Error ("Error unmarshalling JSON" , slog .String ("error" , err .Error ()))
143+ return
144+ }
145+ if len (fioData .Jobs ) > 0 {
146+ slog .Debug ("jobs found in storage benchmark output" )
147+ job := fioData .Jobs [0 ]
148+ readBw = fmt .Sprintf ("%d" , job .Read .Bw / 1024 )
149+ readLat = fmt .Sprintf ("%.0f" , job .Read .LatNs .Mean )
150+ writeBw = fmt .Sprintf ("%d" , job .Write .Bw / 1024 )
151+ writeLat = fmt .Sprintf ("%.0f" , job .Write .LatNs .Mean )
152+ } else {
153+ slog .Error ("No jobs found in storage benchmark output" , slog .String ("output" , output ))
154+ }
155+
156+ slog .Debug ("storage benchmark output" , slog .String ("readLat" , readLat ), slog .String ("readBw" , readBw ), slog .String ("writeLat" , writeLat ), slog .String ("writeBw" , writeBw ))
58157 return
59158}
60159
0 commit comments