Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 65 additions & 2 deletions internal/report/table_helpers_cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,19 @@ type lscpuCacheEntry struct {
CoherencySize int `json:"coherency-size"`
}

// for older lscpu versions that return numbers as strings
type lscpuCacheEntryLegacy struct {
Name string `json:"name"`
OneSize string `json:"one-size"`
AllSize string `json:"all-size"`
Ways string `json:"ways"`
Type string `json:"type"`
Level string `json:"level"`
Sets string `json:"sets"`
PhyLine string `json:"phy-line"`
CoherencySize string `json:"coherency-size"`
}

// parseLscpuCacheOutput parses the output of the `lscpu -C -J` command to extract cache information.
// lscpu returns JSON output with cache details, which this function processes to create a map.
// Example:
Expand Down Expand Up @@ -242,13 +255,63 @@ func parseLscpuCacheOutput(LscpuCacheOutput string) (map[string]lscpuCacheEntry,
return nil, fmt.Errorf("lscpu cache output is empty")
}
output := make(map[string]lscpuCacheEntry)

// Try modern format first (numbers as integers)
parsed := make(map[string][]lscpuCacheEntry)
err := json.Unmarshal([]byte(LscpuCacheOutput), &parsed)
if err == nil {
// Modern format worked
for _, entry := range parsed["caches"] {
output[entry.Name] = entry
}
return output, nil
}

// Fall back to legacy format (numbers as strings)
slog.Debug("Failed to parse lscpu cache output with modern format, trying legacy format", slog.String("error", err.Error()))
parsedLegacy := make(map[string][]lscpuCacheEntryLegacy)
err = json.Unmarshal([]byte(LscpuCacheOutput), &parsedLegacy)
if err != nil {
slog.Error("Failed to parse lscpu cache JSON output", slog.String("error", err.Error()))
slog.Error("Failed to parse lscpu cache JSON output with both modern and legacy formats", slog.String("error", err.Error()))
return nil, err
}
for _, entry := range parsed["caches"] {

// Convert legacy entries to modern format
for _, legacyEntry := range parsedLegacy["caches"] {
entry := lscpuCacheEntry{
Name: legacyEntry.Name,
OneSize: legacyEntry.OneSize,
AllSize: legacyEntry.AllSize,
Type: legacyEntry.Type,
}

// Convert string fields to integers
if legacyEntry.Ways != "" {
if ways, err := strconv.Atoi(legacyEntry.Ways); err == nil {
entry.Ways = ways
}
}
if legacyEntry.Level != "" {
if level, err := strconv.Atoi(legacyEntry.Level); err == nil {
entry.Level = level
}
}
if legacyEntry.Sets != "" {
if sets, err := strconv.Atoi(legacyEntry.Sets); err == nil {
entry.Sets = sets
}
}
if legacyEntry.PhyLine != "" {
if phyLine, err := strconv.Atoi(legacyEntry.PhyLine); err == nil {
entry.PhyLine = phyLine
}
}
if legacyEntry.CoherencySize != "" {
if coherencySize, err := strconv.Atoi(legacyEntry.CoherencySize); err == nil {
entry.CoherencySize = coherencySize
}
}

output[entry.Name] = entry
}
return output, nil
Expand Down
196 changes: 196 additions & 0 deletions internal/report/table_helpers_cache_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ package report

import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestParseCacheSizeToMB(t *testing.T) {
Expand Down Expand Up @@ -42,3 +45,196 @@ func TestParseCacheSizeToMB(t *testing.T) {
}
}
}

func TestParseLscpuCacheOutput(t *testing.T) {
tests := []struct {
name string
input string
expectedError bool
expectedLength int
expectedL3 lscpuCacheEntry
}{
{
name: "Modern format with integer fields",
input: `{
"caches": [
{
"name": "L1d",
"one-size": "48K",
"all-size": "6M",
"ways": 12,
"type": "Data",
"level": 1,
"sets": 64,
"phy-line": 1,
"coherency-size": 64
},
{
"name": "L3",
"one-size": "320M",
"all-size": "640M",
"ways": 20,
"type": "Unified",
"level": 3,
"sets": 262144,
"phy-line": 1,
"coherency-size": 64
}
]
}`,
expectedError: false,
expectedLength: 2,
expectedL3: lscpuCacheEntry{
Name: "L3",
OneSize: "320M",
AllSize: "640M",
Ways: 20,
Type: "Unified",
Level: 3,
Sets: 262144,
PhyLine: 1,
CoherencySize: 64,
},
},
{
name: "Legacy format with string fields",
input: `{
"caches": [
{
"name": "L1d",
"one-size": "48K",
"all-size": "6M",
"ways": "12",
"type": "Data",
"level": "1",
"sets": "64",
"phy-line": "1",
"coherency-size": "64"
},
{
"name": "L3",
"one-size": "320M",
"all-size": "640M",
"ways": "20",
"type": "Unified",
"level": "3",
"sets": "262144",
"phy-line": "1",
"coherency-size": "64"
}
]
}`,
expectedError: false,
expectedLength: 2,
expectedL3: lscpuCacheEntry{
Name: "L3",
OneSize: "320M",
AllSize: "640M",
Ways: 20,
Type: "Unified",
Level: 3,
Sets: 262144,
PhyLine: 1,
CoherencySize: 64,
},
},
{
name: "Legacy format with some empty string fields",
input: `{
"caches": [
{
"name": "L3",
"one-size": "320M",
"all-size": "640M",
"ways": "20",
"type": "Unified",
"level": "3",
"sets": "",
"phy-line": "",
"coherency-size": "64"
}
]
}`,
expectedError: false,
expectedLength: 1,
expectedL3: lscpuCacheEntry{
Name: "L3",
OneSize: "320M",
AllSize: "640M",
Ways: 20,
Type: "Unified",
Level: 3,
Sets: 0, // empty string converts to 0
PhyLine: 0, // empty string converts to 0
CoherencySize: 64,
},
},
{
name: "Legacy format with invalid number strings",
input: `{
"caches": [
{
"name": "L3",
"one-size": "320M",
"all-size": "640M",
"ways": "invalid",
"type": "Unified",
"level": "3",
"sets": "not_a_number",
"phy-line": "1",
"coherency-size": "64"
}
]
}`,
expectedError: false,
expectedLength: 1,
expectedL3: lscpuCacheEntry{
Name: "L3",
OneSize: "320M",
AllSize: "640M",
Ways: 0, // invalid string converts to 0
Type: "Unified",
Level: 3,
Sets: 0, // invalid string converts to 0
PhyLine: 1,
CoherencySize: 64,
},
},
{
name: "Empty input",
input: "",
expectedError: true,
},
{
name: "Invalid JSON",
input: `{"caches": [invalid json}`,
expectedError: true,
},
{
name: "Empty caches array",
input: `{"caches": []}`,
expectedError: false,
expectedLength: 0,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := parseLscpuCacheOutput(tt.input)

if tt.expectedError {
require.Error(t, err)
assert.Nil(t, result)
} else {
require.NoError(t, err)
assert.Len(t, result, tt.expectedLength)

if tt.expectedLength > 0 && tt.expectedL3.Name != "" {
l3Cache, exists := result["L3"]
require.True(t, exists, "L3 cache should exist in result")
assert.Equal(t, tt.expectedL3, l3Cache)
}
}
})
}
}