Skip to content

Commit 8a90ed6

Browse files
committed
feat: add GET /auth/metrics/efficiency endpoint
Add metrics efficiency endpoint that queries miner efficiency (W/TH) data via tailLogCustomRangeAggr RPC. Adds AGGR_FIELDS.EFFICIENCY constant.
1 parent 9342da5 commit 8a90ed6

6 files changed

Lines changed: 264 additions & 5 deletions

File tree

tests/unit/handlers/metrics.handlers.test.js

Lines changed: 139 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@ const {
77
calculateHashrateSummary,
88
getConsumption,
99
processConsumptionData,
10-
calculateConsumptionSummary
10+
calculateConsumptionSummary,
11+
getEfficiency,
12+
processEfficiencyData,
13+
calculateEfficiencySummary
1114
} = require('../../../workers/lib/server/handlers/metrics.handlers')
1215

1316
// ==================== Hashrate Tests ====================
@@ -300,3 +303,138 @@ test('calculateConsumptionSummary - handles empty log', (t) => {
300303
t.is(summary.avgPowerW, null, 'should be null')
301304
t.pass()
302305
})
306+
307+
// ==================== Efficiency Tests ====================
308+
309+
test('getEfficiency - happy path', async (t) => {
310+
const dayTs = 1700006400000
311+
const mockCtx = {
312+
conf: {
313+
orks: [{ rpcPublicKey: 'key1' }]
314+
},
315+
net_r0: {
316+
jRequest: async () => {
317+
return [{ type: 'miner', data: [{ ts: dayTs, val: { efficiency_w_ths_avg_aggr: 25.5 } }], error: null }]
318+
}
319+
}
320+
}
321+
322+
const mockReq = {
323+
query: { start: 1700000000000, end: 1700100000000 }
324+
}
325+
326+
const result = await getEfficiency(mockCtx, mockReq)
327+
t.ok(result.log, 'should return log array')
328+
t.ok(result.summary, 'should return summary')
329+
t.ok(Array.isArray(result.log), 'log should be array')
330+
t.ok(result.log.length > 0, 'log should have entries')
331+
t.is(result.log[0].efficiencyWThs, 25.5, 'should have efficiency value')
332+
t.ok(result.summary.avgEfficiencyWThs !== null, 'should have avg efficiency')
333+
t.pass()
334+
})
335+
336+
test('getEfficiency - missing start throws', async (t) => {
337+
const mockCtx = {
338+
conf: { orks: [] },
339+
net_r0: { jRequest: async () => ({}) }
340+
}
341+
342+
try {
343+
await getEfficiency(mockCtx, { query: { end: 1700100000000 } })
344+
t.fail('should have thrown')
345+
} catch (err) {
346+
t.is(err.message, 'ERR_MISSING_START_END', 'should throw missing start/end error')
347+
}
348+
t.pass()
349+
})
350+
351+
test('getEfficiency - invalid range throws', async (t) => {
352+
const mockCtx = {
353+
conf: { orks: [] },
354+
net_r0: { jRequest: async () => ({}) }
355+
}
356+
357+
try {
358+
await getEfficiency(mockCtx, { query: { start: 1700100000000, end: 1700000000000 } })
359+
t.fail('should have thrown')
360+
} catch (err) {
361+
t.is(err.message, 'ERR_INVALID_DATE_RANGE', 'should throw invalid range error')
362+
}
363+
t.pass()
364+
})
365+
366+
test('getEfficiency - empty ork results', async (t) => {
367+
const mockCtx = {
368+
conf: { orks: [{ rpcPublicKey: 'key1' }] },
369+
net_r0: { jRequest: async () => ({}) }
370+
}
371+
372+
const result = await getEfficiency(mockCtx, { query: { start: 1700000000000, end: 1700100000000 } })
373+
t.ok(result.log, 'should return log array')
374+
t.is(result.log.length, 0, 'log should be empty with no data')
375+
t.is(result.summary.avgEfficiencyWThs, null, 'avg should be null')
376+
t.pass()
377+
})
378+
379+
test('processEfficiencyData - processes array data from ORK', (t) => {
380+
const results = [
381+
[{ type: 'miner', data: [{ ts: 1700006400000, val: { efficiency_w_ths_avg_aggr: 25.5 } }], error: null }]
382+
]
383+
384+
const daily = processEfficiencyData(results)
385+
t.ok(typeof daily === 'object', 'should return object')
386+
t.ok(Object.keys(daily).length > 0, 'should have entries')
387+
const key = Object.keys(daily)[0]
388+
t.is(daily[key].total, 25.5, 'should extract efficiency total')
389+
t.is(daily[key].count, 1, 'should track count')
390+
t.pass()
391+
})
392+
393+
test('processEfficiencyData - processes object-keyed data', (t) => {
394+
const results = [
395+
[{ data: { 1700006400000: { efficiency_w_ths_avg_aggr: 25.5 } } }]
396+
]
397+
398+
const daily = processEfficiencyData(results)
399+
t.ok(typeof daily === 'object', 'should return object')
400+
t.ok(Object.keys(daily).length > 0, 'should have entries')
401+
t.pass()
402+
})
403+
404+
test('processEfficiencyData - handles error results', (t) => {
405+
const results = [{ error: 'timeout' }]
406+
const daily = processEfficiencyData(results)
407+
t.ok(typeof daily === 'object', 'should return object')
408+
t.is(Object.keys(daily).length, 0, 'should be empty for error results')
409+
t.pass()
410+
})
411+
412+
test('processEfficiencyData - averages across multiple orks', (t) => {
413+
const results = [
414+
[{ data: { 1700006400000: { efficiency_w_ths_avg_aggr: 20 } } }],
415+
[{ data: { 1700006400000: { efficiency_w_ths_avg_aggr: 30 } } }]
416+
]
417+
418+
const daily = processEfficiencyData(results)
419+
const key = Object.keys(daily)[0]
420+
t.is(daily[key].total, 50, 'should sum efficiency totals')
421+
t.is(daily[key].count, 2, 'should track count from multiple orks')
422+
t.pass()
423+
})
424+
425+
test('calculateEfficiencySummary - calculates from log entries', (t) => {
426+
const log = [
427+
{ ts: 1700006400000, efficiencyWThs: 25 },
428+
{ ts: 1700092800000, efficiencyWThs: 27 }
429+
]
430+
431+
const summary = calculateEfficiencySummary(log)
432+
t.is(summary.avgEfficiencyWThs, 26, 'should average efficiency')
433+
t.pass()
434+
})
435+
436+
test('calculateEfficiencySummary - handles empty log', (t) => {
437+
const summary = calculateEfficiencySummary([])
438+
t.is(summary.avgEfficiencyWThs, null, 'should be null')
439+
t.pass()
440+
})

tests/unit/routes/metrics.routes.test.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ test('metrics routes - route definitions', (t) => {
1717
const routeUrls = routes.map(route => route.url)
1818
t.ok(routeUrls.includes('/auth/metrics/hashrate'), 'should have hashrate route')
1919
t.ok(routeUrls.includes('/auth/metrics/consumption'), 'should have consumption route')
20+
t.ok(routeUrls.includes('/auth/metrics/efficiency'), 'should have efficiency route')
2021

2122
t.pass()
2223
})

workers/lib/constants.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,8 @@ const ENDPOINTS = {
136136

137137
// Metrics endpoints
138138
METRICS_HASHRATE: '/auth/metrics/hashrate',
139-
METRICS_CONSUMPTION: '/auth/metrics/consumption'
139+
METRICS_CONSUMPTION: '/auth/metrics/consumption',
140+
METRICS_EFFICIENCY: '/auth/metrics/efficiency'
140141
}
141142

142143
const HTTP_METHODS = {
@@ -236,7 +237,8 @@ const AGGR_FIELDS = {
236237
SITE_POWER: 'site_power_w',
237238
ENERGY_AGGR: 'energy_aggr',
238239
ACTIVE_ENERGY_IN: 'active_energy_in_aggr',
239-
UTE_ENERGY: 'ute_energy_aggr'
240+
UTE_ENERGY: 'ute_energy_aggr',
241+
EFFICIENCY: 'efficiency_w_ths_avg_aggr'
240242
}
241243

242244
const PERIOD_TYPES = {

workers/lib/server/handlers/metrics.handlers.js

Lines changed: 92 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,11 +182,102 @@ function calculateConsumptionSummary (log) {
182182
}
183183
}
184184

185+
// ==================== Efficiency ====================
186+
187+
async function getEfficiency (ctx, req) {
188+
const start = Number(req.query.start)
189+
const end = Number(req.query.end)
190+
191+
if (!start || !end) {
192+
throw new Error('ERR_MISSING_START_END')
193+
}
194+
195+
if (start >= end) {
196+
throw new Error('ERR_INVALID_DATE_RANGE')
197+
}
198+
199+
const startDate = new Date(start).toISOString()
200+
const endDate = new Date(end).toISOString()
201+
202+
const results = await requestRpcEachLimit(ctx, RPC_METHODS.TAIL_LOG_RANGE_AGGR, {
203+
keys: [{
204+
type: WORKER_TYPES.MINER,
205+
startDate,
206+
endDate,
207+
fields: { [AGGR_FIELDS.EFFICIENCY]: 1 },
208+
shouldReturnDailyData: 1
209+
}]
210+
})
211+
212+
const daily = processEfficiencyData(results)
213+
const log = Object.keys(daily).sort().map(dayTs => ({
214+
ts: Number(dayTs),
215+
efficiencyWThs: daily[dayTs].total / daily[dayTs].count
216+
}))
217+
218+
const summary = calculateEfficiencySummary(log)
219+
220+
return { log, summary }
221+
}
222+
223+
function processEfficiencyData (results) {
224+
const daily = {}
225+
for (const res of results) {
226+
if (res.error || !res) continue
227+
const data = Array.isArray(res) ? res : (res.data || res.result || [])
228+
if (!Array.isArray(data)) continue
229+
for (const entry of data) {
230+
if (!entry || entry.error) continue
231+
const items = entry.data || entry.items || entry
232+
if (Array.isArray(items)) {
233+
for (const item of items) {
234+
const ts = getStartOfDay(item.ts || item.timestamp)
235+
if (!ts) continue
236+
const val = item.val || item
237+
const eff = val[AGGR_FIELDS.EFFICIENCY] || 0
238+
if (!eff) continue
239+
if (!daily[ts]) daily[ts] = { total: 0, count: 0 }
240+
daily[ts].total += eff
241+
daily[ts].count += 1
242+
}
243+
} else if (typeof items === 'object') {
244+
for (const [key, val] of Object.entries(items)) {
245+
const ts = getStartOfDay(Number(key))
246+
if (!ts) continue
247+
const eff = typeof val === 'object' ? (val[AGGR_FIELDS.EFFICIENCY] || 0) : (Number(val) || 0)
248+
if (!eff) continue
249+
if (!daily[ts]) daily[ts] = { total: 0, count: 0 }
250+
daily[ts].total += eff
251+
daily[ts].count += 1
252+
}
253+
}
254+
}
255+
}
256+
return daily
257+
}
258+
259+
function calculateEfficiencySummary (log) {
260+
if (!log.length) {
261+
return {
262+
avgEfficiencyWThs: null
263+
}
264+
}
265+
266+
const total = log.reduce((sum, entry) => sum + (entry.efficiencyWThs || 0), 0)
267+
268+
return {
269+
avgEfficiencyWThs: safeDiv(total, log.length)
270+
}
271+
}
272+
185273
module.exports = {
186274
getHashrate,
187275
processHashrateData,
188276
calculateHashrateSummary,
189277
getConsumption,
190278
processConsumptionData,
191-
calculateConsumptionSummary
279+
calculateConsumptionSummary,
280+
getEfficiency,
281+
processEfficiencyData,
282+
calculateEfficiencySummary
192283
}

workers/lib/server/routes/metrics.routes.js

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ const {
66
} = require('../../constants')
77
const {
88
getHashrate,
9-
getConsumption
9+
getConsumption,
10+
getEfficiency
1011
} = require('../handlers/metrics.handlers')
1112
const { createCachedAuthRoute } = require('../lib/routeHelpers')
1213

@@ -47,6 +48,23 @@ module.exports = (ctx) => {
4748
ENDPOINTS.METRICS_CONSUMPTION,
4849
getConsumption
4950
)
51+
},
52+
{
53+
method: HTTP_METHODS.GET,
54+
url: ENDPOINTS.METRICS_EFFICIENCY,
55+
schema: {
56+
querystring: schemas.query.efficiency
57+
},
58+
...createCachedAuthRoute(
59+
ctx,
60+
(req) => [
61+
'metrics/efficiency',
62+
req.query.start,
63+
req.query.end
64+
],
65+
ENDPOINTS.METRICS_EFFICIENCY,
66+
getEfficiency
67+
)
5068
}
5169
]
5270
}

workers/lib/server/schemas/metrics.schemas.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,15 @@ const schemas = {
1919
overwriteCache: { type: 'boolean' }
2020
},
2121
required: ['start', 'end']
22+
},
23+
efficiency: {
24+
type: 'object',
25+
properties: {
26+
start: { type: 'integer' },
27+
end: { type: 'integer' },
28+
overwriteCache: { type: 'boolean' }
29+
},
30+
required: ['start', 'end']
2231
}
2332
}
2433
}

0 commit comments

Comments
 (0)