Repository: base/benchmark
Pull request #55: feat: group results by ID
Author: login: patnir
Created: 2025-05-06T22:43:42Z | Merged: 2025-05-07T17:36:55Z

# Description

- adds an ID to benchmark run and results
- displays results grouped by ID where the title is the name of the test

![image](https://github.com/user-attachments/assets/52c7098d-c002-41bf-9495-47c97d751e07)



Diff:
diff --git a/report/src/pages/RunIndex.tsx b/report/src/pages/RunIndex.tsx
index d248adde..d44b87c0 100644
--- a/report/src/pages/RunIndex.tsx
+++ b/report/src/pages/RunIndex.tsx
@@ -7,6 +7,7 @@ import {
 import { useTestMetadata } from "../utils/useDataSeries";
 import { useMemo, useState } from "react";
 import { getBenchmarkVariables } from "../filter";
+import { groupBy } from "lodash";
 
 function RunIndex() {
   const { data: benchmarkRuns, isLoading: isLoadingBenchmarkRuns } =
@@ -39,37 +40,36 @@ function RunIndex() {
     );
   }, [benchmarkRuns, filterSelections]);
 
-  // Calculate only configs that differ across runs
-  const diffConfigKeys = useMemo(() => {
-    if (!matchedRuns) {
-      return [];
-    }
-
-    const configKeyToValues: Record<string, Set<string | number>> = {};
-
+  // Group matchedRuns by id and precompute group sections with diffKeyStart
+  const groupedSections = useMemo(() => {
+    // Group runs by id
     matchedRuns.forEach((run) => {
-      const runConfig = run.testConfig || {};
-      Object.entries(runConfig).forEach(([key, value]) => {
-        if (!configKeyToValues[key]) {
-          configKeyToValues[key] = new Set();
-        }
-        configKeyToValues[key].add(value);
-      });
+      if (!run.id) {
+        run.id = run.outputDir;
+      }
     });
 
-    const differingKeys = Object.entries(configKeyToValues)
-      .filter(
-        ([key, values]) => values.size > 1 || filterSelections[key] !== "any",
-      )
-      .map(([key]) => key);
+    const groups = groupBy(matchedRuns, "id");
 
-    return matchedRuns.map((run) => {
-      const runConfig = run.testConfig || {};
-      return Object.entries(runConfig).filter(([key]) =>
-        differingKeys.includes(key),
-      );
+    // Build sections array with diffKeyStart
+    const sections: {
+      key: string;
+      testName: string;
+      runs: typeof matchedRuns;
+      diffKeyStart: number;
+    }[] = [];
+    let diffKeyStart = 0;
+    Object.entries(groups).forEach(([id, runs]) => {
+      sections.push({
+        key: id,
+        testName: runs[0]?.testName || id,
+        runs,
+        diffKeyStart,
+      });
+      diffKeyStart += runs.length;
     });
-  }, [matchedRuns, filterSelections]);
+    return sections;
+  }, [matchedRuns]);
 
   if (!benchmarkRuns || isLoadingBenchmarkRuns) {
     return <div>Loading...</div>;
@@ -77,194 +77,182 @@ function RunIndex() {
 
   return (
     <div className="container mx-auto">
-      {/* Filter Interface */}
-      <div className="flex flex-wrap gap-4 pb-4 mb-4">
-        {Object.entries(filterOptions)
-          .sort((a, b) => a[0].localeCompare(b[0]))
-          .map(([key, availableValues]) => {
-            const currentValue = filterSelections[key] ?? "any";
-            return (
-              <div key={key}>
-                <div className="text-sm text-slate-500 mb-1">
-                  {camelToTitleCase(key)}
+      <div className="flex justify-between items-center mb-4">
+        {/* Filter Interface */}
+        <div className="flex flex-wrap gap-4 pb-4 mb-4">
+          {Object.entries(filterOptions)
+            .sort((a, b) => a[0].localeCompare(b[0]))
+            .map(([key, availableValues]) => {
+              const currentValue = filterSelections[key] ?? "any";
+              return (
+                <div key={key}>
+                  <div className="text-sm text-slate-500 mb-1">
+                    {camelToTitleCase(key)}
+                  </div>
+                  <select
+                    className="bg-white border border-slate-300 rounded-md px-3 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"
+                    value={String(currentValue)}
+                    onChange={(e) => {
+                      const newValue = e.target.value;
+                      setFilterSelections((prev) => {
+                        const newSelections = { ...prev };
+                        if (newValue === "any") {
+                          delete newSelections[key];
+                        } else {
+                          newSelections[key] = newValue;
+                        }
+                        return newSelections;
+                      });
+                    }}
+                  >
+                    <option value="any">Any</option>
+                    {availableValues.map((val) => (
+                      <option value={String(val)} key={String(val)}>
+                        {formatLabel(String(val))}
+                      </option>
+                    ))}
+                  </select>
                 </div>
-                <select
-                  className="bg-white border border-slate-300 rounded-md px-3 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"
-                  value={String(currentValue)}
-                  onChange={(e) => {
-                    const newValue = e.target.value;
-                    setFilterSelections((prev) => {
-                      const newSelections = { ...prev };
-                      if (newValue === "any") {
-                        delete newSelections[key];
-                      } else {
-                        newSelections[key] = newValue;
-                      }
-                      return newSelections;
-                    });
-                  }}
-                >
-                  <option value="any">Any</option>
-                  {availableValues.map((val) => (
-                    <option value={String(val)} key={String(val)}>
-                      {formatLabel(String(val))}
-                    </option>
-                  ))}
-                </select>
-              </div>
-            );
-          })}
+              );
+            })}
+        </div>
+        <Link to="/run-comparison">
+          <button
+            type="button"
+            className="px-4 py-2 bg-slate-100 text-slate-900 rounded hover:bg-slate-200 transition-colors flex items-center gap-2"
+          >
+            <span role="img" aria-label="Blocks">
+              📊
+            </span>{" "}
+            View Block Metrics
+          </button>
+        </Link>
       </div>
-
-      <table className="min-w-full divide-y divide-slate-200 rounded-lg">
-        <thead>
-          <tr>
-            <th
-              scope="col"
-              className="px-4 py-2 text-left text-xs font-medium text-slate-500 uppercase tracking-wider"
-            >
-              Test Name
-            </th>
-            <th
-              scope="col"
-              className="px-4 py-2 text-left text-xs font-medium text-slate-500 uppercase tracking-wider"
-            >
-              Config
-            </th>
-            <th
-              scope="col"
-              className="px-4 py-2 text-left text-xs font-medium text-slate-500 uppercase tracking-wider"
-            >
-              Status
-            </th>
-            <th
-              scope="col"
-              className="px-4 py-2 text-left text-xs font-medium text-slate-500 uppercase tracking-wider"
-            >
-              Seq. Gas/s
-            </th>
-            <th
-              scope="col"
-              className="px-4 py-2 text-left text-xs font-medium text-slate-500 uppercase tracking-wider"
-            >
-              Send Txs
-            </th>
-            <th
-              scope="col"
-              className="px-4 py-2 text-left text-xs font-medium text-slate-500 uppercase tracking-wider"
-            >
-              Fork Choice
-            </th>
-            <th
-              scope="col"
-              className="px-4 py-2 text-left text-xs font-medium text-slate-500 uppercase tracking-wider"
-            >
-              Get Payload
-            </th>
-            <th
-              scope="col"
-              className="px-4 py-2 text-left text-xs font-medium text-slate-500 uppercase tracking-wider"
-            >
-              Val. Gas/s
-            </th>
-            <th
-              scope="col"
-              className="px-4 py-2 text-left text-xs font-medium text-slate-500 uppercase tracking-wider"
-            >
-              New Payload
-            </th>
-            <th
-              scope="col"
-              className="px-4 py-2 text-left text-xs font-medium text-slate-500 uppercase tracking-wider"
-            >
-              Actions
-            </th>
-          </tr>
-        </thead>
-        <tbody className="bg-white divide-y divide-slate-200 border-left border border-slate-200">
-          {matchedRuns.map((run, i) => (
-            <tr key={run.outputDir} className="hover:bg-slate-50">
-              <td className="px-4 py-3 whitespace-nowrap text-sm font-medium text-slate-900 align-top">
-                <Link to={`/run-comparison`}>{run.testName}</Link>
-              </td>
-              <td className="px-4 py-3 whitespace-nowrap text-sm text-slate-900 align-top">
-                {diffConfigKeys[i] && (
-                  <div className="mt-1 flex flex-wrap gap-1">
-                    {[...diffConfigKeys[i]].map(([key, value]) => (
-                      <span
-                        key={key} // Key should be unique within a run's diff tags
-                        title={`${camelToTitleCase(key)}: ${value}`}
-                        className="inline-flex items-center rounded bg-slate-100 px-1.5 py-0.5 text-xs text-slate-600 ring-1 ring-inset ring-slate-500/10 overflow-hidden text-ellipsis whitespace-nowrap"
-                      >
-                        <span className="mr-1 text-slate-500">
-                          {camelToTitleCase(key)}:
-                        </span>
-                        {key === "GasLimit" ? (
-                          <pre>{formatValue(Number(value), "gas")}</pre>
+      {/* Render all grouped sections */}
+      {groupedSections.map(
+        (section) => (
+          console.log(section),
+          (
+            <div key={section.key} className="mb-10">
+              <h2 className="text-lg font-semibold mb-2">{section.testName}</h2>
+              <p className="text-sm text-slate-500 mb-2">
+                Test ID: {section.key}
+              </p>
+              <table className="min-w-full divide-y divide-slate-200 rounded-lg mb-8">
+                <thead>
+                  <tr>
+                    <th className="px-4 py-2 text-left text-xs font-medium text-slate-500 uppercase tracking-wider">
+                      Config
+                    </th>
+                    <th className="px-4 py-2 text-left text-xs font-medium text-slate-500 uppercase tracking-wider">
+                      Status
+                    </th>
+                    <th className="px-4 py-2 text-left text-xs font-medium text-slate-500 uppercase tracking-wider">
+                      Seq. Gas/s
+                    </th>
+                    <th className="px-4 py-2 text-left text-xs font-medium text-slate-500 uppercase tracking-wider">
+                      Send Txs
+                    </th>
+                    <th className="px-4 py-2 text-left text-xs font-medium text-slate-500 uppercase tracking-wider">
+                      Fork Choice
+                    </th>
+                    <th className="px-4 py-2 text-left text-xs font-medium text-slate-500 uppercase tracking-wider">
+                      Get Payload
+                    </th>
+                    <th className="px-4 py-2 text-left text-xs font-medium text-slate-500 uppercase tracking-wider">
+                      Val. Gas/s
+                    </th>
+                    <th className="px-4 py-2 text-left text-xs font-medium text-slate-500 uppercase tracking-wider">
+                      New Payload
+                    </th>
+                  </tr>
+                </thead>
+                <tbody className="bg-white divide-y divide-slate-200 border-left border border-slate-200">
+                  {section.runs.map((run) => (
+                    <tr key={run.outputDir} className="hover:bg-slate-50">
+                      <td className="px-4 py-3 whitespace-nowrap text-sm text-slate-900 align-top">
+                        {(() => {
+                          return (
+                            <div className="mt-1 flex flex-wrap gap-1">
+                              {Object.entries(run.testConfig || {}).map(
+                                ([key, value]) => (
+                                  <span
+                                    key={key}
+                                    title={`${camelToTitleCase(key)}: ${value}`}
+                                    className="inline-flex items-center rounded bg-slate-100 px-1.5 py-0.5 text-xs text-slate-600 ring-1 ring-inset ring-slate-500/10 overflow-hidden text-ellipsis whitespace-nowrap"
+                                  >
+                                    <span className="mr-1 text-slate-500">
+                                      {camelToTitleCase(key)}:
+                                    </span>
+                                    {key === "GasLimit" ? (
+                                      <pre>
+                                        {formatValue(Number(value), "gas")}
+                                      </pre>
+                                    ) : (
+                                      String(formatLabel(`${value}`))
+                                    )}
+                                  </span>
+                                ),
+                              )}
+                            </div>
+                          );
+                        })()}
+                      </td>
+                      <td className="px-4 py-3 whitespace-nowrap text-sm text-slate-500">
+                        {run.result?.success ? (
+                          <span className="inline-flex items-center rounded-md bg-green-50 px-2 py-1 text-xs font-medium text-green-700 ring-1 ring-inset ring-green-600/20">
+                            Success
+                          </span>
                         ) : (
-                          String(formatLabel(`${value}`))
+                          <span className="inline-flex items-center rounded-md bg-red-50 px-2 py-1 text-xs font-medium text-red-700 ring-1 ring-inset ring-red-600/10">
+                            Failure
+                          </span>
                         )}
-                      </span>
-                    ))}
-                  </div>
-                )}
-              </td>
-              <td className="px-4 py-3 whitespace-nowrap text-sm text-slate-500">
-                {run.result?.success ? (
-                  <span className="inline-flex items-center rounded-md bg-green-50 px-2 py-1 text-xs font-medium text-green-700 ring-1 ring-inset ring-green-600/20">
-                    Success
-                  </span>
-                ) : (
-                  <span className="inline-flex items-center rounded-md bg-red-50 px-2 py-1 text-xs font-medium text-red-700 ring-1 ring-inset ring-red-600/10">
-                    Failure
-                  </span>
-                )}
-              </td>
-              <td className="px-4 py-3 whitespace-nowrap text-sm text-slate-500">
-                {formatValue(
-                  run.result?.sequencerMetrics?.gasPerSecond ?? 0,
-                  "gas/s",
-                )}
-              </td>
-              <td className="px-4 py-3 whitespace-nowrap text-sm text-slate-500">
-                {formatValue(run.result?.sequencerMetrics?.sendTxs ?? 0, "s")}
-              </td>
-              <td className="px-4 py-3 whitespace-nowrap text-sm text-slate-500">
-                {formatValue(
-                  run.result?.sequencerMetrics?.forkChoiceUpdated ?? 0,
-                  "s",
-                )}
-              </td>
-              <td className="px-4 py-3 whitespace-nowrap text-sm text-slate-500">
-                {formatValue(
-                  run.result?.sequencerMetrics?.getPayload ?? 0,
-                  "s",
-                )}
-              </td>
-              <td className="px-4 py-3 whitespace-nowrap text-sm text-slate-500">
-                {formatValue(
-                  run.result?.validatorMetrics?.gasPerSecond ?? 0,
-                  "gas/s",
-                )}
-              </td>
-              <td className="px-4 py-3 whitespace-nowrap text-sm text-slate-500">
-                {formatValue(
-                  run.result?.validatorMetrics?.newPayload ?? 0,
-                  "s",
-                )}
-              </td>
-              <td className="px-4 py-3 whitespace-nowrap text-sm font-medium">
-                <Link
-                  to={`/run-comparison`}
-                  className="text-indigo-600 hover:text-indigo-900"
-                >
-                  Compare
-                </Link>
-              </td>
-            </tr>
-          ))}
-        </tbody>
-      </table>
+                      </td>
+                      <td className="px-4 py-3 whitespace-nowrap text-sm text-slate-500">
+                        {formatValue(
+                          run.result?.sequencerMetrics?.gasPerSecond ?? 0,
+                          "gas/s",
+                        )}
+                      </td>
+                      <td className="px-4 py-3 whitespace-nowrap text-sm text-slate-500">
+                        {formatValue(
+                          run.result?.sequencerMetrics?.sendTxs ?? 0,
+                          "s",
+                        )}
+                      </td>
+                      <td className="px-4 py-3 whitespace-nowrap text-sm text-slate-500">
+                        {formatValue(
+                          run.result?.sequencerMetrics?.forkChoiceUpdated ?? 0,
+                          "s",
+                        )}
+                      </td>
+                      <td className="px-4 py-3 whitespace-nowrap text-sm text-slate-500">
+                        {formatValue(
+                          run.result?.sequencerMetrics?.getPayload ?? 0,
+                          "s",
+                        )}
+                      </td>
+                      <td className="px-4 py-3 whitespace-nowrap text-sm text-slate-500">
+                        {formatValue(
+                          run.result?.validatorMetrics?.gasPerSecond ?? 0,
+                          "gas/s",
+                        )}
+                      </td>
+                      <td className="px-4 py-3 whitespace-nowrap text-sm text-slate-500">
+                        {formatValue(
+                          run.result?.validatorMetrics?.newPayload ?? 0,
+                          "s",
+                        )}
+                      </td>
+                    </tr>
+                  ))}
+                </tbody>
+              </table>
+            </div>
+          )
+        ),
+      )}
     </div>
   );
 }
diff --git a/report/src/types.ts b/report/src/types.ts
index 997c8ea7..667e3112 100644
--- a/report/src/types.ts
+++ b/report/src/types.ts
@@ -40,6 +40,7 @@ export interface ChartConfig {
 }
 
 export interface BenchmarkRun {
+  id: string;
   sourceFile: string;
   testName: string;
   testDescription: string;
diff --git a/runner/benchmark/benchmark.go b/runner/benchmark/benchmark.go
index 2679c8fc..d495ec09 100644
--- a/runner/benchmark/benchmark.go
+++ b/runner/benchmark/benchmark.go
@@ -36,6 +36,7 @@ func (p Params) ToConfig() map[string]interface{} {
 
 // TestRun is a single run of a benchmark. Each config should result in multiple test runs.
 type TestRun struct {
+	ID          string
 	Params      Params
 	TestFile    string
 	Name        string
diff --git a/runner/benchmark/matrix.go b/runner/benchmark/matrix.go
index 182fd1eb..759ab2d5 100644
--- a/runner/benchmark/matrix.go
+++ b/runner/benchmark/matrix.go
@@ -195,6 +195,8 @@ func ResolveTestRunsFromMatrix(c TestDefinition, testFileName string) ([]TestRun
 
 	testOutDir := fmt.Sprintf("%s-%s-%d", nameToSlug(fileNameWithoutExt), nameToSlug(c.Name), time.Now().Unix())
 
+	id := fmt.Sprintf("%s-%s-%d", nameToSlug(fileNameWithoutExt), nameToSlug(c.Name), time.Now().Unix())
+
 	for i := 0; i < totalParams; i++ {
 		valueSelections := make(map[ParamType]interface{})
 		for j, p := range params {
@@ -207,6 +209,7 @@ func ResolveTestRunsFromMatrix(c TestDefinition, testFileName string) ([]TestRun
 		}
 
 		testParams[i] = TestRun{
+			ID:          id,
 			Params:      *params,
 			OutputDir:   fmt.Sprintf("%s-%d", testOutDir, i),
 			Name:        c.Name,
diff --git a/runner/benchmark/matrix_test.go b/runner/benchmark/matrix_test.go
index a621ca48..8a2066dc 100644
--- a/runner/benchmark/matrix_test.go
+++ b/runner/benchmark/matrix_test.go
@@ -128,12 +128,14 @@ func TestResolveTestRunsFromMatrix(t *testing.T) {
 			} else {
 				require.NoError(t, err)
 			}
-			// ignore outputDir
+			// ignore outputDir and id
 			for i := range tt.want {
 				tt.want[i].OutputDir = ""
+				tt.want[i].ID = ""
 			}
 			for i := range got {
 				got[i].OutputDir = ""
+				got[i].ID = ""
 			}
 			require.ElementsMatch(t, tt.want, got)
 		})
diff --git a/runner/benchmark/result_metadata.go b/runner/benchmark/result_metadata.go
index 1cf055dd..a9254d28 100644
--- a/runner/benchmark/result_metadata.go
+++ b/runner/benchmark/result_metadata.go
@@ -26,6 +26,7 @@ type BenchmarkRunResult struct {
 
 // BenchmarkRun is the output JSON metadata for a benchmark run.
 type BenchmarkRun struct {
+	ID              string                 `json:"id"`
 	SourceFile      string                 `json:"sourceFile"`
 	OutputDir       string                 `json:"outputDir"`
 	TestName        string                 `json:"testName"`
@@ -56,6 +57,7 @@ func BenchmarkMetadataFromTestPlan(testPlan TestPlan) BenchmarkRuns {
 
 	for _, params := range testPlan {
 		metadata.Runs = append(metadata.Runs, BenchmarkRun{
+			ID:              params.ID,
 			SourceFile:      params.TestFile,
 			TestName:        params.Name,
 			TestDescription: params.Description,


Reviews:
user: login: meyer9
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-06T23:44:55Z
user: login: patnir
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-07T15:48:01Z
user: login: meyer9
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-07T17:04:13Z
user: login: meyer9
state: DISMISSED
author_association: COLLABORATOR
submitted_at: 2025-05-07T17:14:53Z
user: login: meyer9
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-07T17:36:35Z

Review comments:
diff_hunk: @@ -253,18 +220,78 @@ function RunIndex() {
                   "s",
                 )}
               </td>
-              <td className="px-4 py-3 whitespace-nowrap text-sm font-medium">
-                <Link
-                  to={`/run-comparison`}
-                  className="text-indigo-600 hover:text-indigo-900"
-                >
-                  Compare
-                </Link>
-              </td>
             </tr>
           ))}
         </tbody>
       </table>
+    );
+  }
+
+  if (!benchmarkRuns || isLoadingBenchmarkRuns) {
+    return <div>Loading...</div>;
+  }
+
+  return (
+    <div className="container mx-auto">
+      <div className="flex justify-between items-center mb-4">
+        {/* Filter Interface */}
+        <div className="flex flex-wrap gap-4 pb-4 mb-4">
+          {Object.entries(filterOptions)
+            .sort((a, b) => a[0].localeCompare(b[0]))
+            .map(([key, availableValues]) => {
+              const currentValue = filterSelections[key] ?? "any";
+              return (
+                <div key={key}>
+                  <div className="text-sm text-slate-500 mb-1">
+                    {camelToTitleCase(key)}
+                  </div>
+                  <select
+                    className="bg-white border border-slate-300 rounded-md px-3 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"
+                    value={String(currentValue)}
+                    onChange={(e) => {
+                      const newValue = e.target.value;
+                      setFilterSelections((prev) => {
+                        const newSelections = { ...prev };
+                        if (newValue === "any") {
+                          delete newSelections[key];
+                        } else {
+                          newSelections[key] = newValue;
+                        }
+                        return newSelections;
+                      });
+                    }}
+                  >
+                    <option value="any">Any</option>
+                    {availableValues.map((val) => (
+                      <option value={String(val)} key={String(val)}>
+                        {formatLabel(String(val))}
+                      </option>
+                    ))}
+                  </select>
+                </div>
+              );
+            })}
+        </div>
+        <Link to="/run-comparison">
+          <button
+            type="button"
+            className="px-4 py-2 bg-slate-100 text-slate-900 rounded hover:bg-slate-200 transition-colors flex items-center gap-2"
+          >
+            <span role="img" aria-label="Blocks">
+              📊
+            </span>{" "}
+            View Block Metrics
+          </button>
+        </Link>
+      </div>
+      {/* Render all grouped sections */}
+      {groupedSections.map((section) => (
+        <div key={section.key} className="mb-10">
+          <h2 className="text-lg font-semibold mb-2">{section.testName}</h2>
+          <p className="text-sm text-slate-500 mb-2">Test ID: {section.key}</p>
+          {renderRunTable(section.runs, diffConfigKeys, section.diffKeyStart)}
path: report/src/pages/RunIndex.tsx
user: login: meyer9
body: Since this is the only usage, I'd just inline this here
created_at: 2025-05-06T23:40:54Z
author_association: COLLABORATOR
diff_hunk: @@ -71,128 +72,94 @@ function RunIndex() {
     });
   }, [matchedRuns, filterSelections]);
 
-  if (!benchmarkRuns || isLoadingBenchmarkRuns) {
-    return <div>Loading...</div>;
-  }
-
-  return (
-    <div className="container mx-auto">
-      {/* Filter Interface */}
-      <div className="flex flex-wrap gap-4 pb-4 mb-4">
-        {Object.entries(filterOptions)
-          .sort((a, b) => a[0].localeCompare(b[0]))
-          .map(([key, availableValues]) => {
-            const currentValue = filterSelections[key] ?? "any";
-            return (
-              <div key={key}>
-                <div className="text-sm text-slate-500 mb-1">
-                  {camelToTitleCase(key)}
-                </div>
-                <select
-                  className="bg-white border border-slate-300 rounded-md px-3 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"
-                  value={String(currentValue)}
-                  onChange={(e) => {
-                    const newValue = e.target.value;
-                    setFilterSelections((prev) => {
-                      const newSelections = { ...prev };
-                      if (newValue === "any") {
-                        delete newSelections[key];
-                      } else {
-                        newSelections[key] = newValue;
-                      }
-                      return newSelections;
-                    });
-                  }}
-                >
-                  <option value="any">Any</option>
-                  {availableValues.map((val) => (
-                    <option value={String(val)} key={String(val)}>
-                      {formatLabel(String(val))}
-                    </option>
-                  ))}
-                </select>
-              </div>
-            );
-          })}
-      </div>
+  // Group matchedRuns by id and precompute group sections with diffKeyStart
+  const groupedSections = useMemo(() => {
+    // Group runs by id
+    const groups: Record<string, BenchmarkRun[]> = {};
path: report/src/pages/RunIndex.tsx
user: login: meyer9
body: nit: could possibly simplify this using `_.groupBy` https://lodash.com/docs/#groupBy. For the grouping key, you can use `(group) => group.id ?? null`, which should result in a grouping with type `Record<string | null, BenchmarkRun[]>`
created_at: 2025-05-06T23:44:05Z
author_association: COLLABORATOR
diff_hunk: @@ -40,6 +40,7 @@ export interface ChartConfig {
 }
 
 export interface BenchmarkRun {
+  id?: string;
path: report/src/types.ts
user: login: meyer9
body: We can probably make this required - up to you though, or maybe there's a reason not to
created_at: 2025-05-06T23:44:47Z
author_association: COLLABORATOR
diff_hunk: @@ -40,6 +40,7 @@ export interface ChartConfig {
 }
 
 export interface BenchmarkRun {
+  id?: string;
path: report/src/types.ts
user: login: patnir
body: Just FYI then we need to add IDs to the previous results or generate new ones after this gets merged
created_at: 2025-05-07T15:48:01Z
author_association: COLLABORATOR
diff_hunk: @@ -40,6 +40,7 @@ export interface ChartConfig {
 }
 
 export interface BenchmarkRun {
+  id?: string;
path: report/src/types.ts
user: login: meyer9
body: Yeah, good point! That should be fine since CI generates a new result each time before publishing. In the future, we will want this to be backwards compatible with old results though.
created_at: 2025-05-07T17:04:13Z
author_association: COLLABORATOR
diff_hunk: @@ -39,232 +40,221 @@ function RunIndex() {
     );
   }, [benchmarkRuns, filterSelections]);
 
-  // Calculate only configs that differ across runs
-  const diffConfigKeys = useMemo(() => {
-    if (!matchedRuns) {
-      return [];
-    }
-
-    const configKeyToValues: Record<string, Set<string | number>> = {};
-
+  // Group matchedRuns by id and precompute group sections with diffKeyStart
+  const groupedSections = useMemo(() => {
+    // Group runs by id
     matchedRuns.forEach((run) => {
-      const runConfig = run.testConfig || {};
-      Object.entries(runConfig).forEach(([key, value]) => {
-        if (!configKeyToValues[key]) {
-          configKeyToValues[key] = new Set();
-        }
-        configKeyToValues[key].add(value);
-      });
+      if (!run.id) {
+        run.id = run.outputDir;
+      }
     });
 
-    const differingKeys = Object.entries(configKeyToValues)
-      .filter(
-        ([key, values]) => values.size > 1 || filterSelections[key] !== "any",
-      )
-      .map(([key]) => key);
+    const groups = groupBy(matchedRuns, "id");
 
-    return matchedRuns.map((run) => {
-      const runConfig = run.testConfig || {};
-      return Object.entries(runConfig).filter(([key]) =>
-        differingKeys.includes(key),
-      );
+    // Build sections array with diffKeyStart
+    const sections: {
+      key: string;
+      testName: string;
+      runs: typeof matchedRuns;
+      diffKeyStart: number;
+    }[] = [];
+    let diffKeyStart = 0;
+    Object.entries(groups).forEach(([id, runs]) => {
+      sections.push({
+        key: id,
+        testName: runs[0]?.testName || id,
+        runs,
+        diffKeyStart,
+      });
+      diffKeyStart += runs.length;
     });
-  }, [matchedRuns, filterSelections]);
+
+    console.log(sections);
path: report/src/pages/RunIndex.tsx
user: login: meyer9
body: nit remove
created_at: 2025-05-07T17:13:57Z
author_association: COLLABORATOR

PR comments:
user: login: cb-heimdall
created_at: 2025-05-06T22:43:47Z
body: <h1>✅ Heimdall Review Status</h1>
<table>
<tr>
<th>Requirement</th>
<th>Status</th>
<th>More Info</th>
</tr>
<tr>
<td>Reviews</td>
<td>
✅
<code>1/1</code>
</td>
<td>
<details>
<summary>Denominator calculation</summary>
<table>
<tr>
<td>
<details>
<summary>Show calculation</summary>
<table>
<tr>
<td><code>1</code> if user is bot</td>
<td>0</td>
</tr>
<tr>
<td><code>1</code> if user is external</td>
<td>0</td>
</tr>
<tr>
<td>
From <code>.codeflow.yml</code>
</td>
<td>1</td>
</tr>
<tr>
<td>
Additional review requirements
<details>
<summary>Show calculation</summary>
<table>
<tr>
<td><strong>Max</strong></td>
<td><strong>0</strong></td>
</tr>
</table>
</details>
</td>
<td>0</td>
</tr>
<tr>
<td>
From <code>CODEOWNERS</code>
</td>
<td>0</td>
</tr>
<tr>
<td>Global minimum</td>
<td>0</td>
</tr>
<tr>
<td><strong>Max</strong></td>
<td>
<strong>
1
</strong>
</td>
</tr>
</table>
</details>
</td>
<td>1</td>
</tr>
<tr>
<td><code>1</code> if commit is unverified</td>
<td>0</td>
</tr>
<tr>
<td><strong>Sum</strong></td>
<td><strong>1</strong></td>
</tr>
</table>
</details>
</td>
</tr>
</table>
<!-- CODEOWNERS & Bar Raisers -->
<!-- Power Rangers -->
<!-- Links -->

author_association: COLLABORATOR
user: login: patnir
created_at: 2025-05-07T17:10:01Z
body: @meyer9 I removed the filtering of diff config keys, what do you think? I find that its easier to have context of the results that way
author_association: COLLABORATOR
user: login: meyer9
created_at: 2025-05-07T17:15:02Z
body: LG! Just one small commnet
author_association: COLLABORATOR