-
Notifications
You must be signed in to change notification settings - Fork 10
feat: add cpu power query & subscription #1745
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1401,6 +1401,27 @@ type CpuUtilization implements Node { | |
cpus: [CpuLoad!]! | ||
} | ||
|
||
type CpuPower implements Node { | ||
id: PrefixedID! | ||
|
||
"""CPU power in watts""" | ||
totalPower: Float | ||
|
||
"""CPU power in watts for each physical processor""" | ||
coresPower: [Float!] | ||
} | ||
|
||
type CpuPackages implements Node { | ||
"""Total CPU package power draw (W)""" | ||
totalpower: number | ||
|
||
"""Power draw per package (W)""" | ||
power: [Float!] | ||
|
||
"""description: 'Temperature per package (°C)""" | ||
temp: [Float!] | ||
} | ||
Comment on lines
+1414
to
+1423
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chainSchema blocker: invalid type and missing
Apply these changes in the code-first model (e.g., type CpuPackages implements Node {
- """Total CPU package power draw (W)"""
- totalpower: number
+ id: PrefixedID!
+ """Total CPU package power draw (W)"""
+ totalPower: Float
"""Power draw per package (W)"""
power: [Float!]
- """description: 'Temperature per package (°C)"""
+ """Temperature per package (°C)"""
temp: [Float!]
} Then re-run codegen to fix the pipeline error. Verification: 🏁 Script executed: #!/bin/bash
# Fail build if invalid scalar 'number' remains in schema
rg -n ':[[:space:]]*number\b' api/generated-schema.graphql && exit 1 || echo "OK: no 'number' scalars"
# Regenerate (project-specific)
pnpm run generate || npm run generate || yarn generate Length of output: 25 🏁 Script executed: #!/bin/bash
# Get full CpuPackages type definition with context
rg -n -A 15 'type CpuPackages' api/generated-schema.graphql
# Check if InfoCpu type exists and its fields
rg -n -A 10 'type InfoCpu' api/generated-schema.graphql
# Search for code-first model files (TypeScript/TS)
fd -e ts -e js --type f | xargs rg -l 'CpuPackages|class.*Cpu' | head -20 Length of output: 1000 🏁 Script executed: #!/bin/bash
# Get full InfoCpu definition (may span multiple lines)
rg -n -A 30 'type InfoCpu implements Node' api/generated-schema.graphql | head -50
# Inspect the code-first model to see source definitions
cat api/src/unraid-api/graph/resolvers/info/cpu/cpu.model.ts Length of output: 5068 Schema blocker: invalid The code-first model (
Update the model: -@ObjectType()
+@ObjectType({ implements: () => Node })
-export class CpuPackages {
- @Field(() => Float, { description: 'Total CPU package power draw (W)' })
- totalpower?: number;
+export class CpuPackages extends Node {
+ @Field(() => Float, { description: 'Total CPU package power draw (W)' })
+ totalPower?: number; Then regenerate the schema.
🤖 Prompt for AI Agents
|
||
|
||
type InfoCpu implements Node { | ||
id: PrefixedID! | ||
|
||
|
@@ -1446,6 +1467,9 @@ type InfoCpu implements Node { | |
"""Number of physical processors""" | ||
processors: Int | ||
|
||
"""CPU power information""" | ||
power: CpuPower | ||
|
||
Comment on lines
+1470
to
+1472
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chainAdd missing fields to InfoCpu or remove them from resolver.
Proposed additions in the model: type InfoCpu implements Node {
...
"""CPU power information"""
power: CpuPower
+ """Per-package power/temperature telemetry"""
+ packages: CpuPackages
+ """Per-package core/thread groupings"""
+ topology: [[[Int!]!]!]
...
} Ensure I'll search the codebase to verify the review comment. Let me examine the GraphQL schema and the CpuService implementation. 🏁 Script executed: #!/bin/bash
# Find and examine the GraphQL schema file
fd -e graphql -e gql | head -20 Length of output: 71 🏁 Script executed: #!/bin/bash
# Search for CpuService and generateCpu implementation
rg -n "CpuService|generateCpu" --type=ts --type=js -B2 -A5 | head -100 Length of output: 11144 🏁 Script executed: #!/bin/bash
# Look for InfoCpu type definition in schema files
rg -n "type InfoCpu|interface InfoCpu" --type=graphql -A10 -B2 Length of output: 31 🏁 Script executed: #!/bin/bash
# Search for topology and packages field definitions
rg -n "topology|packages" --type=graphql -B2 -A2 | head -80 Length of output: 31 🏁 Script executed: #!/bin/bash
# Look for generateTopology function
rg -n "generateTopology" --type=ts --type=js -B2 -A10 Length of output: 2937 🏁 Script executed: #!/bin/bash
# Examine the specific line range in schema file if it exists
if [ -f "api/generated-schema.graphql" ]; then
sed -n '1465,1480p' api/generated-schema.graphql
fi Length of output: 251 🏁 Script executed: #!/bin/bash
# Search for CpuPackages type definition
rg -n "type CpuPackages|interface CpuPackages" --type=graphql -B2 -A5 Length of output: 31 🏁 Script executed: #!/bin/bash
# Look for CpuPower type to understand context
rg -n "type CpuPower" --type=graphql -B2 -A10 Length of output: 31 🏁 Script executed: #!/bin/bash
# Find all CPU-related service files
fd -i cpu --type=f | grep -E "\.(ts|js|graphql)$" | head -30 Length of output: 421 🏁 Script executed: #!/bin/bash
# Search for return statements in generateCpu to see what's being returned
rg -n "return.*packages|return.*topology" --type=ts --type=js -B5 -A2 Length of output: 0 🏁 Script executed: #!/bin/bash
# Look for CpuPower definition to understand existing structure
rg -n "CpuPower" --type=graphql -B2 -A8 Length of output: 31 🏁 Script executed: #!/bin/bash
# Search for InfoCpu in resolver or service files
rg -n "InfoCpu" --type=ts --type=js -B3 -A3 | head -60 Length of output: 4280 🏁 Script executed: #!/bin/bash
# Look for function that returns cpu data with packages/topology
rg -n "packages.*topology|topology.*packages" --type=ts --type=js -B5 -A5 Length of output: 0 🏁 Script executed: #!/bin/bash
# Check what the current schema structure shows for CPU
rg -n "\"CPU" --type=graphql -B2 -A5 | head -50 Length of output: 31 🏁 Script executed: #!/bin/bash
# Look for all fields in InfoCpu type
rg -n "type InfoCpu" --type=graphql -A50 | head -80 Length of output: 31 🏁 Script executed: #!/bin/bash
# Search for topology array definition pattern
rg -n "\[\[\[" --type=graphql -B2 -A2 Length of output: 31 🏁 Script executed: #!/bin/bash
# Find resolver files for CPU
fd resolver.*cpu|fd cpu.*resolver --type=f | head -20 Length of output: 0 🏁 Script executed: #!/bin/bash
# Look for info.ts or cpu.ts files that might contain service logic
fd -e ts -e js | grep -iE "(info|cpu)" | head -30 Length of output: 1891 🏁 Script executed: #!/bin/bash
# Search for classes or exported functions related to CpuService
ast-grep --pattern 'class CpuService { $$$ }' Length of output: 5923 🏁 Script executed: #!/bin/bash
# Look for object property definitions related to cpu packages and topology
rg -n "packages\s*:|topology\s*:" --type=ts --type=js -B3 -A3 | head -80 Length of output: 3468 🏁 Script executed: #!/bin/bash
# Check the exact structure being returned from generateCpu
rg -n "generateCpu\s*\(|function generateCpu" --type=ts --type=js -A30 | head -100 Length of output: 9916 🏁 Script executed: #!/bin/bash
# Look at all type definitions in graphql file
fd -e graphql -exec wc -l {} \; | sort -rn | head -10 Length of output: 137 🏁 Script executed: #!/bin/bash
# Get line count and examine schema structure
if [ -f "api/generated-schema.graphql" ]; then
echo "=== Total lines in schema ===";
wc -l api/generated-schema.graphql;
echo "=== Content around line 1470 ===";
sed -n '1460,1480p' api/generated-schema.graphql;
fi Length of output: 423 🏁 Script executed: #!/bin/bash
# Search for all imports and usages of CpuPower, CpuPackages
rg -n "CpuPower|CpuPackages" --type=graphql --type=ts --type=js -B1 -A1 Length of output: 31 🏁 Script executed: #!/bin/bash
# Look for any TODO or FIXME comments related to cpu schema
rg -n "TODO|FIXME" --type=graphql --type=ts --type=js | grep -i cpu Length of output: 31 🏁 Script executed: #!/bin/bash
# Check file structure to understand path organization
find . -type f -name "*.graphql" -o -name "*cpu*" -o -name "*service*" | grep -v node_modules | head -40 Length of output: 2221 🏁 Script executed: #!/bin/bash
# Look for schema merge or composition files
fd -e graphql -e schema | grep -iE "(index|merge|compose|generated)" | head -20 Length of output: 71 🏁 Script executed: #!/bin/bash
# Examine CpuService class methods
rg -n "class CpuService" --type=ts --type=js -A100 | head -150 Length of output: 5923 🏁 Script executed: #!/bin/bash
# Search for property definitions that might be causing the mismatch
rg -n "power.*:|packages.*:|topology.*:" --type=ts --type=js -B2 -A2 | head -100 Length of output: 18057 🏁 Script executed: #!/bin/bash
# Look for destructuring or object construction with these fields
rg -n "\{.*power.*packages.*topology|\{.*packages.*topology" --type=ts --type=js -B3 -A3 Length of output: 0 🏁 Script executed: #!/bin/bash
# Final check: search for all field assignments in context of cpu object construction
rg -n "power:|packages:|topology:" --type=ts --type=js -B5 | head -80 Length of output: 7160 🏁 Script executed: #!/bin/bash
# Check for any property spread or merge operations
rg -n "\.\.\." --type=ts --type=js | grep -i cpu -A2 -B2 | head -40 Length of output: 1070 🏁 Script executed: #!/bin/bash
# Look for GraphQL type definitions that match InfoCpu pattern
rg -n "type.*Cpu.*{|interface.*Cpu.*{" --type=graphql -A20 | head -150 Length of output: 124 🏁 Script executed: #!/bin/bash
# Search for resolver implementation files
fd -type f -name "*.ts" -o -name "*.js" | xargs grep -l "InfoCpu\|CpuService" | head -20 Length of output: 170 🏁 Script executed: #!/bin/bash
# Get the exact structure around line 1470
if [ -f "api/generated-schema.graphql" ]; then
nl -ba api/generated-schema.graphql | sed -n '1465,1475p'
fi Length of output: 40 🏁 Script executed: #!/bin/bash
# Look for all union or interface implementations
rg -n "implements Node" --type=graphql -B5 -A30 | grep -A30 "InfoCpu" Length of output: 31 🏁 Script executed: #!/bin/bash
# Find what entity extends or implements what
rg -n "InfoCpu|CpuInfo|Cpu" --type=graphql | head -50 Length of output: 31 🏁 Script executed: #!/bin/bash
# Check broader search for node implementations
rg -n "implements Node" --type=graphql | head -30 Length of output: 31 🏁 Script executed: #!/bin/bash
# List all files in api directory
ls -la api/ 2>/dev/null | head -30 Length of output: 1861 🏁 Script executed: #!/bin/bash
# Get all graphql files and their structure
find . -name "*.graphql" -type f | head -20 Length of output: 75 🏁 Script executed: #!/bin/bash
# Locate GraphQL schema and CpuService files
fd -type f \( -name "*.graphql" -o -name "*cpu*" \) 2>/dev/null | head -30 Length of output: 0 🏁 Script executed: #!/bin/bash
# Search for InfoCpu type and CpuService class
rg -n "type InfoCpu|class CpuService" --type graphql --type ts --type js -A 15 2>/dev/null | head -80 Length of output: 0 🏁 Script executed: #!/bin/bash
# Search for generateCpu method
rg -n "generateCpu\s*\(" --type ts --type js -A 20 2>/dev/null | head -100 Length of output: 9370 🏁 Script executed: #!/bin/bash
# Search for packages and topology fields in services/resolvers
rg -n "packages|topology" --type ts --type js -B 2 -A 2 2>/dev/null | head -80 Length of output: 15193 🏁 Script executed: #!/bin/bash
# Check the specific schema file if it exists
if [ -f "api/generated-schema.graphql" ]; then
echo "=== Lines 1465-1480 ===";
sed -n '1465,1480p' api/generated-schema.graphql
fi Length of output: 275 🏁 Script executed: #!/bin/bash
# Search for generateTopology function
rg -n "generateTopology" --type ts --type js -A 10 2>/dev/null Length of output: 2455 🏁 Script executed: #!/bin/bash
# Search for CpuPackages type definition
rg -n "type CpuPackages" --type graphql -A 8 2>/dev/null Length of output: 0 🏁 Script executed: #!/bin/bash
# Look at resolver files that might map service to schema
rg -n "CpuService|cpu.*resolver" --type ts --type js -l 2>/dev/null | head -10 Length of output: 636 🏁 Script executed: #!/bin/bash
# Check for object construction patterns with power, packages, topology
rg -n "power\s*:|packages\s*:|topology\s*:" --type ts --type js -B 3 2>/dev/null | head -60 Length of output: 4938 🏁 Script executed: #!/bin/bash
# List all files in api directory to find schema
ls -la api/ 2>/dev/null Length of output: 1994 🏁 Script executed: #!/bin/bash
# Broader search for CPU-related resolvers or services
find . -path ./node_modules -prune -o -type f \( -name "*.ts" -o -name "*.js" \) -print 2>/dev/null | xargs grep -l "InfoCpu\|generateCpu" 2>/dev/null | head -20 Length of output: 764 🏁 Script executed: #!/bin/bash
# Search for return statements in CPU-related functions
rg -n "return\s*\{" --type ts --type js -A 10 2>/dev/null | grep -B 5 -A 10 "power\|packages\|topology" | head -80 Length of output: 7508 Schema is out of sync with TypeScript model— The verification confirms the issue: The schema file needs regeneration or manual update to include these fields so they're exposed via GraphQL queries. 🤖 Prompt for AI Agents
|
||
"""CPU socket type""" | ||
socket: String | ||
|
||
|
@@ -2642,6 +2666,8 @@ type Subscription { | |
arraySubscription: UnraidArray! | ||
logFile(path: String!): LogFileContent! | ||
systemMetricsCpu: CpuUtilization! | ||
systemMetricsCpuPower: CpuPower! | ||
systemMetricsCpuTelemetry: CpuPackages! | ||
systemMetricsMemory: MemoryUtilization! | ||
upsUpdates: UPSDevice! | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,136 @@ | ||
import { Injectable, Logger } from '@nestjs/common'; | ||
import { constants as fsConstants } from 'node:fs'; | ||
import { access, readdir, readFile } from 'node:fs/promises'; | ||
import { basename, join } from 'path'; | ||
|
||
import { CpuPower } from '@app/unraid-api/graph/resolvers/info/cpu/cpu.model.js'; | ||
|
||
@Injectable() | ||
export class CpuPowerService { | ||
private readonly logger = new Logger(CpuPowerService.name); | ||
async generateCpuPower(): Promise<CpuPower> { | ||
const id = 'info/cpu-power'; | ||
try { | ||
const powercapDir = '/sys/class/powercap'; | ||
const prefixes = ['intel-rapl', 'intel-rapl-mmio', 'amd-rapl']; | ||
|
||
const dirEntries = await readdir(powercapDir, { withFileTypes: true }).catch(() => []); | ||
const raplDomains = dirEntries | ||
.filter((d) => prefixes.some((p) => d.name.startsWith(`${p}:`))) | ||
.map((d) => join(powercapDir, d.name)); | ||
|
||
if (!raplDomains.length) { | ||
this.logger.warn(`No RAPL domains found: ${JSON.stringify(dirEntries, null, 2)}`); | ||
return { id }; | ||
} | ||
this.logger.debug(JSON.stringify(raplDomains, null, 2)); | ||
|
||
const prevEnergy = new Map<string, number>(); | ||
const prevTime = new Map<string, bigint>(); | ||
|
||
for (const domainPath of raplDomains) { | ||
const energyFile = join(domainPath, 'energy_uj'); | ||
try { | ||
await access(energyFile, fsConstants.R_OK); | ||
const content = await readFile(energyFile, 'utf8'); | ||
const energy = Number.parseInt(content.trim(), 10); | ||
if (!Number.isNaN(energy)) { | ||
prevEnergy.set(domainPath, energy); | ||
prevTime.set(domainPath, process.hrtime.bigint()); | ||
} | ||
} catch { | ||
// ignore unreadable files | ||
} | ||
} | ||
|
||
// ~200ms delay to measure energy delta | ||
await new Promise((resolve) => setTimeout(resolve, 200)); | ||
|
||
const packages = new Map<number, Record<string, number>>(); | ||
|
||
for (const domainPath of raplDomains) { | ||
const energyFile = join(domainPath, 'energy_uj'); | ||
const nameFile = join(domainPath, 'name'); | ||
|
||
try { | ||
await access(energyFile, fsConstants.R_OK); | ||
await access(nameFile, fsConstants.R_OK); | ||
} catch { | ||
continue; | ||
} | ||
|
||
const label = (await readFile(nameFile, 'utf8')).trim(); | ||
const nowStr = await readFile(energyFile, 'utf8'); | ||
const now = Number.parseInt(nowStr.trim(), 10); | ||
const tNow = process.hrtime.bigint(); | ||
|
||
const prevE = prevEnergy.get(domainPath); | ||
const prevT = prevTime.get(domainPath); | ||
if (prevE === undefined || prevT === undefined) continue; | ||
|
||
const diffEnergy = now - prevE; // microjoules | ||
const diffTimeNs = Number(tNow - prevT); // nanoseconds | ||
if (!(diffTimeNs > 0)) continue; | ||
|
||
let powerW = (diffEnergy * 1e-6) / (diffTimeNs * 1e-9); | ||
if (!Number.isFinite(powerW) || powerW < 0) continue; | ||
powerW = Math.round(powerW * 100) / 100; | ||
|
||
let pkgId = 0; | ||
const m1 = label.match(/^package-(\d+)$/); | ||
if (m1) { | ||
pkgId = Number(m1[1]); | ||
} else { | ||
const base = basename(domainPath); | ||
const m2 = base.match(/:(\d+)/); | ||
if (m2) { | ||
pkgId = Number(m2[1]); | ||
} else { | ||
const m3 = domainPath.match(/:(\d+)(?:\/|$)/); | ||
if (m3) pkgId = Number(m3[1]); | ||
} | ||
} | ||
|
||
if (!packages.has(pkgId)) packages.set(pkgId, {}); | ||
const pkg = packages.get(pkgId)!; | ||
if (/^package-\d+$/.test(label)) { | ||
pkg['package'] = powerW; | ||
} else { | ||
pkg[label] = powerW; | ||
} | ||
} | ||
|
||
if (!packages.size) { | ||
return { id }; | ||
} | ||
|
||
let total = 0; | ||
const sortedPkgIds = [...packages.keys()].sort((a, b) => a - b); | ||
const coresPower: number[] = []; | ||
|
||
for (const pkgId of sortedPkgIds) { | ||
const pkg = packages.get(pkgId)!; | ||
if (pkg['package'] === undefined) { | ||
const sum = Object.values(pkg) | ||
.filter((v) => typeof v === 'number' && Number.isFinite(v)) | ||
.reduce((a, b) => a + b, 0); | ||
if (sum > 0) { | ||
pkg['package'] = Math.round(sum * 100) / 100; | ||
} | ||
} | ||
if (pkg['package'] !== undefined) { | ||
total += pkg['package']; | ||
coresPower.push(pkg['package']); | ||
} | ||
} | ||
|
||
const result: CpuPower = { id, coresPower: coresPower.length ? coresPower : undefined }; | ||
if (sortedPkgIds.length > 0 && total > 0) { | ||
result.totalPower = Math.round(total * 100) / 100; | ||
} | ||
return result; | ||
} catch { | ||
return { id }; | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change | |||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,208 @@ | |||||||||||||||||||||||||||||
import { Injectable, Logger } from '@nestjs/common'; | |||||||||||||||||||||||||||||
import { promises as fs } from 'fs'; | |||||||||||||||||||||||||||||
import { join } from 'path'; | |||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||
const { readdir, readFile, access } = fs; | |||||||||||||||||||||||||||||
const { constants: fsConstants } = fs; | |||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||
@Injectable() | |||||||||||||||||||||||||||||
export class CpuTopologyService { | |||||||||||||||||||||||||||||
private readonly logger = new Logger(CpuTopologyService.name); | |||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||
private topologyCache: { id: number; cores: number[][] }[] | null = null; | |||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||
// ----------------------------------------------------------------- | |||||||||||||||||||||||||||||
// Read static CPU topology, per-package core thread pairs | |||||||||||||||||||||||||||||
// ----------------------------------------------------------------- | |||||||||||||||||||||||||||||
async generateTopology(): Promise<number[][][]> { | |||||||||||||||||||||||||||||
const packages: Record<number, number[][]> = {}; | |||||||||||||||||||||||||||||
const cpuDirs = await readdir('/sys/devices/system/cpu'); | |||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||
for (const dir of cpuDirs) { | |||||||||||||||||||||||||||||
if (!/^cpu\d+$/.test(dir)) continue; | |||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||
const basePath = join('/sys/devices/system/cpu', dir, 'topology'); | |||||||||||||||||||||||||||||
const pkgFile = join(basePath, 'physical_package_id'); | |||||||||||||||||||||||||||||
const siblingsFile = join(basePath, 'thread_siblings_list'); | |||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||
try { | |||||||||||||||||||||||||||||
const [pkgIdStr, siblingsStrRaw] = await Promise.all([ | |||||||||||||||||||||||||||||
readFile(pkgFile, 'utf8'), | |||||||||||||||||||||||||||||
readFile(siblingsFile, 'utf8'), | |||||||||||||||||||||||||||||
]); | |||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||
const pkgId = parseInt(pkgIdStr.trim(), 10); | |||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||
// expand ranges | |||||||||||||||||||||||||||||
const siblings = siblingsStrRaw | |||||||||||||||||||||||||||||
.trim() | |||||||||||||||||||||||||||||
.replace(/(\d+)-(\d+)/g, (_, start, end) => | |||||||||||||||||||||||||||||
Array.from( | |||||||||||||||||||||||||||||
{ length: parseInt(end) - parseInt(start) + 1 }, | |||||||||||||||||||||||||||||
(_, i) => parseInt(start) + i | |||||||||||||||||||||||||||||
).join(',') | |||||||||||||||||||||||||||||
) | |||||||||||||||||||||||||||||
.split(',') | |||||||||||||||||||||||||||||
.map((n) => parseInt(n, 10)); | |||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||
if (!packages[pkgId]) packages[pkgId] = []; | |||||||||||||||||||||||||||||
if (!packages[pkgId].some((arr) => arr.join(',') === siblings.join(','))) { | |||||||||||||||||||||||||||||
packages[pkgId].push(siblings); | |||||||||||||||||||||||||||||
} | |||||||||||||||||||||||||||||
} catch (err) { | |||||||||||||||||||||||||||||
console.warn('Topology read error for', dir, err); | |||||||||||||||||||||||||||||
} | |||||||||||||||||||||||||||||
} | |||||||||||||||||||||||||||||
// Sort cores within each package, and packages by their lowest core index | |||||||||||||||||||||||||||||
const result = Object.entries(packages) | |||||||||||||||||||||||||||||
.sort((a, b) => a[1][0][0] - b[1][0][0]) // sort packages by first CPU ID | |||||||||||||||||||||||||||||
.map( | |||||||||||||||||||||||||||||
([pkgId, cores]) => cores.sort((a, b) => a[0] - b[0]) // sort cores within package | |||||||||||||||||||||||||||||
); | |||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||
return result; | |||||||||||||||||||||||||||||
} | |||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||
// ----------------------------------------------------------------- | |||||||||||||||||||||||||||||
// Dynamic telemetry (power + temperature) | |||||||||||||||||||||||||||||
// ----------------------------------------------------------------- | |||||||||||||||||||||||||||||
private async getPackageTemps(): Promise<number[]> { | |||||||||||||||||||||||||||||
const temps: number[] = []; | |||||||||||||||||||||||||||||
try { | |||||||||||||||||||||||||||||
const hwmons = await readdir('/sys/class/hwmon'); | |||||||||||||||||||||||||||||
for (const hwmon of hwmons) { | |||||||||||||||||||||||||||||
const path = join('/sys/class/hwmon', hwmon); | |||||||||||||||||||||||||||||
try { | |||||||||||||||||||||||||||||
const label = (await readFile(join(path, 'name'), 'utf8')).trim(); | |||||||||||||||||||||||||||||
if (/coretemp|k10temp|zenpower/i.test(label)) { | |||||||||||||||||||||||||||||
const files = await readdir(path); | |||||||||||||||||||||||||||||
for (const f of files) { | |||||||||||||||||||||||||||||
if (f.startsWith('temp') && f.endsWith('_label')) { | |||||||||||||||||||||||||||||
const lbl = (await readFile(join(path, f), 'utf8')).trim().toLowerCase(); | |||||||||||||||||||||||||||||
if ( | |||||||||||||||||||||||||||||
lbl.includes('package id') || | |||||||||||||||||||||||||||||
lbl.includes('tctl') || | |||||||||||||||||||||||||||||
lbl.includes('tdie') | |||||||||||||||||||||||||||||
) { | |||||||||||||||||||||||||||||
const inputFile = join(path, f.replace('_label', '_input')); | |||||||||||||||||||||||||||||
try { | |||||||||||||||||||||||||||||
const raw = await readFile(inputFile, 'utf8'); | |||||||||||||||||||||||||||||
temps.push(parseInt(raw.trim(), 10) / 1000); | |||||||||||||||||||||||||||||
} catch (err) { | |||||||||||||||||||||||||||||
this.logger.warn('Failed to read file', err); | |||||||||||||||||||||||||||||
} | |||||||||||||||||||||||||||||
} | |||||||||||||||||||||||||||||
} | |||||||||||||||||||||||||||||
} | |||||||||||||||||||||||||||||
} | |||||||||||||||||||||||||||||
} catch (err) { | |||||||||||||||||||||||||||||
this.logger.warn('Failed to read file', err); | |||||||||||||||||||||||||||||
} | |||||||||||||||||||||||||||||
} | |||||||||||||||||||||||||||||
} catch (err) { | |||||||||||||||||||||||||||||
this.logger.warn('Failed to read file', err); | |||||||||||||||||||||||||||||
} | |||||||||||||||||||||||||||||
return temps; | |||||||||||||||||||||||||||||
} | |||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||
private async getPackagePower(): Promise<Record<number, Record<string, number>>> { | |||||||||||||||||||||||||||||
const basePath = '/sys/class/powercap'; | |||||||||||||||||||||||||||||
const prefixes = ['intel-rapl', 'intel-rapl-mmio', 'amd-rapl']; | |||||||||||||||||||||||||||||
const raplPaths: string[] = []; | |||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||
try { | |||||||||||||||||||||||||||||
const entries = await readdir(basePath, { withFileTypes: true }); | |||||||||||||||||||||||||||||
for (const entry of entries) { | |||||||||||||||||||||||||||||
if (entry.isSymbolicLink() && prefixes.some((p) => entry.name.startsWith(p))) { | |||||||||||||||||||||||||||||
if (/:\d+:\d+/.test(entry.name)) continue; | |||||||||||||||||||||||||||||
raplPaths.push(join(basePath, entry.name)); | |||||||||||||||||||||||||||||
} | |||||||||||||||||||||||||||||
} | |||||||||||||||||||||||||||||
} catch { | |||||||||||||||||||||||||||||
return {}; | |||||||||||||||||||||||||||||
} | |||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||
if (!raplPaths.length) return {}; | |||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||
const readEnergy = async (p: string): Promise<number | null> => { | |||||||||||||||||||||||||||||
try { | |||||||||||||||||||||||||||||
await access(join(p, 'energy_uj'), fsConstants.R_OK); | |||||||||||||||||||||||||||||
const raw = await readFile(join(p, 'energy_uj'), 'utf8'); | |||||||||||||||||||||||||||||
return parseInt(raw.trim(), 10); | |||||||||||||||||||||||||||||
} catch { | |||||||||||||||||||||||||||||
return null; | |||||||||||||||||||||||||||||
} | |||||||||||||||||||||||||||||
}; | |||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||
const prevE = new Map<string, number>(); | |||||||||||||||||||||||||||||
const prevT = new Map<string, bigint>(); | |||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||
for (const p of raplPaths) { | |||||||||||||||||||||||||||||
const val = await readEnergy(p); | |||||||||||||||||||||||||||||
if (val !== null) { | |||||||||||||||||||||||||||||
prevE.set(p, val); | |||||||||||||||||||||||||||||
prevT.set(p, process.hrtime.bigint()); | |||||||||||||||||||||||||||||
} | |||||||||||||||||||||||||||||
} | |||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||
await new Promise((res) => setTimeout(res, 100)); | |||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||
const results: Record<number, Record<string, number>> = {}; | |||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||
for (const p of raplPaths) { | |||||||||||||||||||||||||||||
const now = await readEnergy(p); | |||||||||||||||||||||||||||||
if (now === null) continue; | |||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||
const prevVal = prevE.get(p); | |||||||||||||||||||||||||||||
const prevTime = prevT.get(p); | |||||||||||||||||||||||||||||
if (prevVal === undefined || prevTime === undefined) continue; | |||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||
const diffE = now - prevVal; | |||||||||||||||||||||||||||||
const diffT = Number(process.hrtime.bigint() - prevTime); | |||||||||||||||||||||||||||||
if (diffT <= 0 || diffE < 0) continue; | |||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||
const watts = (diffE * 1e-6) / (diffT * 1e-9); | |||||||||||||||||||||||||||||
const powerW = Math.round(watts * 100) / 100; | |||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||
const nameFile = join(p, 'name'); | |||||||||||||||||||||||||||||
let label = 'package'; | |||||||||||||||||||||||||||||
try { | |||||||||||||||||||||||||||||
label = (await readFile(nameFile, 'utf8')).trim(); | |||||||||||||||||||||||||||||
} catch (err) { | |||||||||||||||||||||||||||||
this.logger.warn('Failed to read file', err); | |||||||||||||||||||||||||||||
} | |||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||
const pkgMatch = label.match(/package-(\d+)/i); | |||||||||||||||||||||||||||||
const pkgId = pkgMatch ? Number(pkgMatch[1]) : 0; | |||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||
if (!results[pkgId]) results[pkgId] = {}; | |||||||||||||||||||||||||||||
results[pkgId][label] = powerW; | |||||||||||||||||||||||||||||
} | |||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||
for (const [pkgId, domains] of Object.entries(results)) { | |||||||||||||||||||||||||||||
Check noticeCode scanning / CodeQL Unused variable, import, function or class Note
Unused variable pkgId.
Copilot AutofixAI 1 day ago To fix the problem, we simply need to remove the unused variable No imports or method changes are required; just this structure edit in the body of the class in the given file.
Suggested changeset
1
api/src/unraid-api/graph/resolvers/info/cpu/cpu-topology.service.ts
Copilot is powered by AI and may make mistakes. Always verify output.
Unable to commit as this autofix suggestion is now outdated
Positive FeedbackNegative Feedback
Refresh and try again.
|
|||||||||||||||||||||||||||||
const total = Object.values(domains).reduce((a, b) => a + b, 0); | |||||||||||||||||||||||||||||
(domains as any)['total'] = Math.round(total * 100) / 100; | |||||||||||||||||||||||||||||
} | |||||||||||||||||||||||||||||
Comment on lines
+182
to
+185
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Remove unused loop variable or use Avoid - for (const [pkgId, domains] of Object.entries(results)) {
- const total = Object.values(domains).reduce((a, b) => a + b, 0);
- (domains as any)['total'] = Math.round(total * 100) / 100;
- }
+ for (const domains of Object.values(results)) {
+ const total = Object.values(domains).reduce((a, b) => a + b, 0);
+ (domains as any)['total'] = Math.round(total * 100) / 100;
+ } 📝 Committable suggestion
Suggested change
🧰 Tools🪛 GitHub Check: CodeQL[notice] 182-182: Unused variable, import, function or class 🤖 Prompt for AI Agents
|
|||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||
return results; | |||||||||||||||||||||||||||||
} | |||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||
async generateTelemetry(): Promise<{ id: number; power: number; temp: number }[]> { | |||||||||||||||||||||||||||||
const temps = await this.getPackageTemps(); | |||||||||||||||||||||||||||||
const powerData = await this.getPackagePower(); | |||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||
const maxPkg = Math.max(temps.length - 1, ...Object.keys(powerData).map(Number), 0); | |||||||||||||||||||||||||||||
const result = []; | |||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||
for (let pkgId = 0; pkgId <= maxPkg; pkgId++) { | |||||||||||||||||||||||||||||
const entry = powerData[pkgId] ?? {}; | |||||||||||||||||||||||||||||
result.push({ | |||||||||||||||||||||||||||||
id: pkgId, | |||||||||||||||||||||||||||||
power: entry.total ?? -1, | |||||||||||||||||||||||||||||
temp: temps[pkgId] ?? -1, | |||||||||||||||||||||||||||||
}); | |||||||||||||||||||||||||||||
} | |||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||
return result; | |||||||||||||||||||||||||||||
Comment on lines
+195
to
+206
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fix TS typing: Add an explicit type so - const result = [];
+ const result: { id: number; power: number; temp: number }[] = []; 🧰 Tools🪛 GitHub Actions: CI - Main (API)[error] 199-199: TS2345: Argument of type '{ id: number; power: number; temp: number; }' is not assignable to parameter of type 'never'. 🤖 Prompt for AI Agents
|
|||||||||||||||||||||||||||||
} | |||||||||||||||||||||||||||||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
Regenerate client GraphQL types to include CpuPower and systemMetricsCpuPower.
Schema adds CpuPower and systemMetricsCpuPower, but client types in snippets don’t include them. Regenerate CLI/web GraphQL typings to avoid runtime/type errors.
Run to verify and locate missing/updated references:
Also applies to: 1459-1461, 2658-2658
🏁 Script executed:
Length of output: 254
🏁 Script executed:
Length of output: 17850
Regenerate GraphQL client typings for CLI, plugin, and web: Subscription type in generated outputs (
api/src/unraid-api/cli/generated/graphql.ts
,packages/unraid-api-plugin-connect/src/graphql/generated/client/graphql.ts
,web/composables/gql/graphql.ts
,web/src/composables/gql/graphql.ts
) is missing the newsystemMetricsCpuPower
andCpuPower
fields. Rerun your GraphQL code generation to include them.🤖 Prompt for AI Agents