04: metrics chunking and dashboards
Improve mining metrics with chunked progress updates and dashboards - Emit periodic per-thread progress updates by chunking search ranges, enabling fresher and more accurate job/global hash rates. - Smooth per-thread hash rates with EMA; derive per-job and global rates from thread aggregates. - Remove job/thread rate series on completion/cancellation to avoid scrape-timing zero artifacts. - Add service-level metrics: `miner_active_jobs` gauge and `miner_mine_requests_total` counter with result labels. - Introduce CLI/env knob `--progress-chunk-ms` to control progress update frequency (default 2000ms). - Update Grafana dashboards for trend-friendly queries, cleaned labels, 24h/1h peak views, and job-level filtering. - Add detailed documentation summarizing metrics improvements, usage, and operational guidance.
This commit is contained in:
@@ -19,10 +19,15 @@
|
||||
//! - The HTTP exporter is spawned as a background task and does not block.
|
||||
//! - If the `http-exporter` feature is disabled, `start_http_exporter` becomes
|
||||
//! a no-op that returns immediately.
|
||||
//! - When jobs/threads end, prefer removing gauge label children (series) rather than
|
||||
//! writing a zero; this avoids scrape-timing artifacts and produces cleaner rollups.
|
||||
//! - Service-level observability: expose an `active_jobs` gauge and a `mine_requests_total`
|
||||
//! counter (labeled by result) to disambiguate "idle because no jobs" vs "actively mining".
|
||||
|
||||
use once_cell::sync::Lazy;
|
||||
use prometheus::{
|
||||
opts, Encoder, Gauge, GaugeVec, IntCounter, IntCounterVec, IntGauge, IntGaugeVec, Registry, TextEncoder,
|
||||
opts, Encoder, Gauge, GaugeVec, IntCounter, IntCounterVec, IntGauge, IntGaugeVec, Registry,
|
||||
TextEncoder,
|
||||
};
|
||||
|
||||
#[cfg(feature = "http-exporter")]
|
||||
@@ -42,9 +47,43 @@ use anyhow::Result;
|
||||
|
||||
static REGISTRY: Lazy<Registry> = Lazy::new(Registry::new);
|
||||
|
||||
// -------------------------------------------------------------------------------------
|
||||
// High-level service metrics
|
||||
// -------------------------------------------------------------------------------------
|
||||
|
||||
static ACTIVE_JOBS: Lazy<IntGauge> = Lazy::new(|| {
|
||||
let g = IntGauge::new(
|
||||
"miner_active_jobs",
|
||||
"Number of currently running mining jobs",
|
||||
)
|
||||
.expect("create miner_active_jobs");
|
||||
REGISTRY
|
||||
.register(Box::new(g.clone()))
|
||||
.expect("register miner_active_jobs");
|
||||
g
|
||||
});
|
||||
|
||||
static MINE_REQUESTS_TOTAL: Lazy<IntCounterVec> = Lazy::new(|| {
|
||||
let c = IntCounterVec::new(
|
||||
opts!(
|
||||
"miner_mine_requests_total",
|
||||
"Count of /mine requests by result"
|
||||
),
|
||||
&["result"], // accepted, duplicate, invalid, error
|
||||
)
|
||||
.expect("create miner_mine_requests_total");
|
||||
REGISTRY
|
||||
.register(Box::new(c.clone()))
|
||||
.expect("register miner_mine_requests_total");
|
||||
c
|
||||
});
|
||||
|
||||
static JOBS_TOTAL: Lazy<IntCounterVec> = Lazy::new(|| {
|
||||
let c = IntCounterVec::new(opts!("miner_jobs_total", "Number of jobs by status"), &["status"])
|
||||
.expect("create miner_jobs_total");
|
||||
let c = IntCounterVec::new(
|
||||
opts!("miner_jobs_total", "Number of jobs by status"),
|
||||
&["status"],
|
||||
)
|
||||
.expect("create miner_jobs_total");
|
||||
REGISTRY
|
||||
.register(Box::new(c.clone()))
|
||||
.expect("register miner_jobs_total");
|
||||
@@ -187,6 +226,15 @@ pub fn default_registry() -> &'static Registry {
|
||||
|
||||
/// Increment the total hashes counter by `n` (number of nonces tested).
|
||||
// Labeled helpers for engine/job/thread metrics
|
||||
//
|
||||
// Service-level helpers
|
||||
pub fn set_active_jobs(n: i64) {
|
||||
ACTIVE_JOBS.set(n);
|
||||
}
|
||||
|
||||
pub fn inc_mine_requests(result: &str) {
|
||||
MINE_REQUESTS_TOTAL.with_label_values(&[result]).inc();
|
||||
}
|
||||
|
||||
pub fn inc_job_hashes(engine: &str, job_id: &str, n: u64) {
|
||||
JOB_HASHES_TOTAL
|
||||
@@ -195,9 +243,7 @@ pub fn inc_job_hashes(engine: &str, job_id: &str, n: u64) {
|
||||
}
|
||||
|
||||
pub fn set_job_hash_rate(engine: &str, job_id: &str, rate: f64) {
|
||||
JOB_HASH_RATE
|
||||
.with_label_values(&[engine, job_id])
|
||||
.set(rate);
|
||||
JOB_HASH_RATE.with_label_values(&[engine, job_id]).set(rate);
|
||||
}
|
||||
|
||||
pub fn inc_thread_hashes(engine: &str, job_id: &str, thread_id: &str, n: u64) {
|
||||
@@ -245,6 +291,20 @@ pub fn inc_http_request(endpoint: &str, code: u16) {
|
||||
.inc();
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------------------
|
||||
// Removal helpers for end-of-life series
|
||||
// -------------------------------------------------------------------------------------
|
||||
|
||||
/// Remove the per-job hash rate series for a finished job.
|
||||
pub fn remove_job_hash_rate(engine: &str, job_id: &str) {
|
||||
let _ = JOB_HASH_RATE.remove_label_values(&[engine, job_id]);
|
||||
}
|
||||
|
||||
/// Remove the per-thread hash rate series for a finished thread.
|
||||
pub fn remove_thread_hash_rate(engine: &str, job_id: &str, thread_id: &str) {
|
||||
let _ = THREAD_HASH_RATE.remove_label_values(&[engine, job_id, thread_id]);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------------------
|
||||
// HTTP Exporter (feature: http-exporter)
|
||||
// -------------------------------------------------------------------------------------
|
||||
@@ -270,7 +330,9 @@ pub async fn start_http_exporter(port: u16) -> Result<()> {
|
||||
let encoder = TextEncoder::new();
|
||||
let metric_families = REGISTRY.gather();
|
||||
let mut buffer = Vec::with_capacity(16 * 1024);
|
||||
encoder.encode(&metric_families, &mut buffer).unwrap_or_default();
|
||||
encoder
|
||||
.encode(&metric_families, &mut buffer)
|
||||
.unwrap_or_default();
|
||||
|
||||
Response::builder()
|
||||
.header("Content-Type", encoder.format_type())
|
||||
|
||||
@@ -18,6 +18,11 @@ struct Args {
|
||||
#[arg(long, env = "MINER_METRICS_PORT")]
|
||||
metrics_port: Option<u16>,
|
||||
|
||||
/// Target milliseconds for per-thread progress updates (chunking).
|
||||
/// Smaller values increase metrics freshness but add a bit of overhead.
|
||||
#[arg(long = "progress-chunk-ms", env = "MINER_PROGRESS_CHUNK_MS")]
|
||||
progress_chunk_ms: Option<u64>,
|
||||
|
||||
/// Mining engine to use (default: cpu-fast). Options: cpu-baseline, cpu-fast
|
||||
#[arg(long, env = "MINER_ENGINE", value_enum, default_value_t = EngineCli::CpuFast)]
|
||||
engine: EngineCli,
|
||||
@@ -60,7 +65,9 @@ async fn main() {
|
||||
log::info!("API listening port: {}", args.port);
|
||||
match args.num_cores {
|
||||
Some(n) if n > 0 => log::info!("Using specified number of cores: {}", n),
|
||||
Some(_) => log::warn!("Number of cores must be positive. Defaulting to all available cores."),
|
||||
Some(_) => {
|
||||
log::warn!("Number of cores must be positive. Defaulting to all available cores.")
|
||||
}
|
||||
None => log::info!("Using all available cores (auto-detected)."),
|
||||
}
|
||||
match args.metrics_port {
|
||||
@@ -73,6 +80,7 @@ async fn main() {
|
||||
port: args.port,
|
||||
num_cores: args.num_cores,
|
||||
metrics_port: args.metrics_port,
|
||||
progress_chunk_ms: args.progress_chunk_ms,
|
||||
engine: args.engine.into(),
|
||||
};
|
||||
|
||||
|
||||
+218
-49
@@ -24,6 +24,8 @@ pub struct ServiceConfig {
|
||||
pub num_cores: Option<usize>,
|
||||
/// Optional metrics port. When Some, metrics endpoint starts; when None, metrics are disabled.
|
||||
pub metrics_port: Option<u16>,
|
||||
/// Target milliseconds for per-thread progress updates (chunking). If None, defaults to 2000ms.
|
||||
pub progress_chunk_ms: Option<u64>,
|
||||
/// Engine selection (future use). For now, CPU baseline/fast engines are supported.
|
||||
pub engine: EngineSelection,
|
||||
}
|
||||
@@ -43,6 +45,7 @@ impl Default for ServiceConfig {
|
||||
port: 9833,
|
||||
num_cores: None,
|
||||
metrics_port: None,
|
||||
progress_chunk_ms: None,
|
||||
engine: EngineSelection::CpuBaseline,
|
||||
}
|
||||
}
|
||||
@@ -54,14 +57,20 @@ pub struct MiningService {
|
||||
pub jobs: Arc<Mutex<HashMap<String, MiningJob>>>,
|
||||
pub num_cores: usize,
|
||||
pub engine: Arc<dyn MinerEngine>,
|
||||
/// Target milliseconds for per-thread progress updates (chunking).
|
||||
pub progress_chunk_ms: u64,
|
||||
/// Gauge of currently running jobs (for metrics)
|
||||
pub active_jobs_gauge: Arc<tokio::sync::Mutex<i64>>,
|
||||
}
|
||||
|
||||
impl MiningService {
|
||||
pub fn new(num_cores: usize, engine: Arc<dyn MinerEngine>) -> Self {
|
||||
pub fn new(num_cores: usize, engine: Arc<dyn MinerEngine>, progress_chunk_ms: u64) -> Self {
|
||||
Self {
|
||||
jobs: Arc::new(Mutex::new(HashMap::new())),
|
||||
num_cores,
|
||||
engine,
|
||||
progress_chunk_ms,
|
||||
active_jobs_gauge: Arc::new(tokio::sync::Mutex::new(0)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,8 +89,14 @@ impl MiningService {
|
||||
metrics::set_job_status_gauge(self.engine.name(), &job_id, "completed", 0);
|
||||
metrics::set_job_status_gauge(self.engine.name(), &job_id, "failed", 0);
|
||||
metrics::set_job_status_gauge(self.engine.name(), &job_id, "cancelled", 0);
|
||||
// increment active jobs
|
||||
{
|
||||
let mut g = self.active_jobs_gauge.lock().await;
|
||||
*g += 1;
|
||||
metrics::set_active_jobs(*g);
|
||||
}
|
||||
}
|
||||
job.start_mining(self.engine.clone(), self.num_cores);
|
||||
job.start_mining(self.engine.clone(), self.num_cores, self.progress_chunk_ms);
|
||||
jobs.insert(job_id, job);
|
||||
Ok(())
|
||||
}
|
||||
@@ -96,6 +111,14 @@ impl MiningService {
|
||||
if let Some(mut job) = jobs.remove(job_id) {
|
||||
log::debug!(target: "miner", "Removing job: {}", job_id);
|
||||
job.cancel();
|
||||
#[cfg(feature = "metrics")]
|
||||
{
|
||||
let mut g = self.active_jobs_gauge.lock().await;
|
||||
if *g > 0 {
|
||||
*g -= 1;
|
||||
}
|
||||
metrics::set_active_jobs(*g);
|
||||
}
|
||||
Some(job)
|
||||
} else {
|
||||
None
|
||||
@@ -106,6 +129,14 @@ impl MiningService {
|
||||
let mut jobs = self.jobs.lock().await;
|
||||
if let Some(job) = jobs.get_mut(job_id) {
|
||||
job.cancel();
|
||||
#[cfg(feature = "metrics")]
|
||||
{
|
||||
let mut g = self.active_jobs_gauge.lock().await;
|
||||
if *g > 0 {
|
||||
*g -= 1;
|
||||
}
|
||||
metrics::set_active_jobs(*g);
|
||||
}
|
||||
true
|
||||
} else {
|
||||
false
|
||||
@@ -144,15 +175,12 @@ impl MiningService {
|
||||
#[cfg(feature = "metrics")]
|
||||
{
|
||||
// Aggregate and per-job hash-rate estimate across running jobs (nonces/sec)
|
||||
// Aggregate hash-rate across running jobs from last recorded per-job rates
|
||||
let mut total_rate = 0.0;
|
||||
for (job_id, job) in jobs_guard.iter() {
|
||||
if job.status == JobStatus::Running {
|
||||
let elapsed = job.start_time.elapsed().as_secs_f64();
|
||||
if elapsed > 0.0 {
|
||||
let rate = job.total_hash_count as f64 / elapsed;
|
||||
total_rate += rate;
|
||||
metrics::set_job_hash_rate(job.engine_name, job_id, rate);
|
||||
}
|
||||
total_rate += job.last_hash_rate;
|
||||
metrics::set_job_hash_rate(job.engine_name, job_id, job.last_hash_rate);
|
||||
}
|
||||
}
|
||||
metrics::set_hash_rate(total_rate);
|
||||
@@ -192,6 +220,7 @@ pub struct MiningJob {
|
||||
pub status: JobStatus,
|
||||
pub start_time: Instant,
|
||||
pub total_hash_count: u64,
|
||||
pub last_hash_rate: f64,
|
||||
pub best_result: Option<MiningJobResult>,
|
||||
|
||||
pub engine_name: &'static str,
|
||||
@@ -215,6 +244,7 @@ impl Clone for MiningJob {
|
||||
status: self.status.clone(),
|
||||
start_time: self.start_time,
|
||||
total_hash_count: self.total_hash_count,
|
||||
last_hash_rate: self.last_hash_rate,
|
||||
best_result: self.best_result.clone(),
|
||||
engine_name: self.engine_name,
|
||||
job_id: self.job_id.clone(),
|
||||
@@ -253,6 +283,7 @@ impl MiningJob {
|
||||
status: JobStatus::Running,
|
||||
start_time: Instant::now(),
|
||||
total_hash_count: 0,
|
||||
last_hash_rate: 0.0,
|
||||
best_result: None,
|
||||
engine_name: "unknown",
|
||||
job_id: None,
|
||||
@@ -265,7 +296,12 @@ impl MiningJob {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start_mining(&mut self, engine: Arc<dyn MinerEngine>, num_cores: usize) {
|
||||
pub fn start_mining(
|
||||
&mut self,
|
||||
engine: Arc<dyn MinerEngine>,
|
||||
num_cores: usize,
|
||||
progress_chunk_ms: u64,
|
||||
) {
|
||||
let (sender, receiver) = bounded(num_cores * 2);
|
||||
self.result_receiver = Some(receiver);
|
||||
self.engine_name = engine.name();
|
||||
@@ -300,6 +336,7 @@ impl MiningJob {
|
||||
let sender = sender.clone();
|
||||
let ctx = ctx.clone();
|
||||
let engine = engine.clone();
|
||||
let progress_chunk_ms = progress_chunk_ms;
|
||||
|
||||
let handle = thread::spawn(move || {
|
||||
mine_range_with_engine(
|
||||
@@ -309,6 +346,7 @@ impl MiningJob {
|
||||
EngineRange { start, end },
|
||||
cancel_flag,
|
||||
sender,
|
||||
progress_chunk_ms,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -329,6 +367,15 @@ impl MiningJob {
|
||||
metrics::set_job_status_gauge(self.engine_name, job_id, "completed", 0);
|
||||
metrics::set_job_status_gauge(self.engine_name, job_id, "failed", 0);
|
||||
metrics::set_job_status_gauge(self.engine_name, job_id, "cancelled", 1);
|
||||
// Remove job hash rate series on cancellation (avoid scrape-timing artifacts)
|
||||
self.last_hash_rate = 0.0;
|
||||
metrics::remove_job_hash_rate(self.engine_name, job_id);
|
||||
// Remove all per-thread hash rate series on cancellation and clear tracking
|
||||
for (tid, _) in self.thread_rate_ema.iter() {
|
||||
metrics::remove_thread_hash_rate(self.engine_name, job_id, &tid.to_string());
|
||||
}
|
||||
self.thread_last_update.clear();
|
||||
self.thread_rate_ema.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -372,7 +419,8 @@ impl MiningJob {
|
||||
.get(&thread_result.thread_id)
|
||||
.copied()
|
||||
.unwrap_or(instant_rate);
|
||||
let ema = THREAD_RATE_EMA_ALPHA * instant_rate + (1.0 - THREAD_RATE_EMA_ALPHA) * prev;
|
||||
let ema = THREAD_RATE_EMA_ALPHA * instant_rate
|
||||
+ (1.0 - THREAD_RATE_EMA_ALPHA) * prev;
|
||||
metrics::set_thread_hash_rate(
|
||||
self.engine_name,
|
||||
job_id,
|
||||
@@ -380,12 +428,15 @@ impl MiningJob {
|
||||
ema,
|
||||
);
|
||||
// Store updated EMA for next delta
|
||||
self.thread_rate_ema
|
||||
.insert(thread_result.thread_id, ema);
|
||||
self.thread_rate_ema.insert(thread_result.thread_id, ema);
|
||||
}
|
||||
// Update last-seen timestamp for this thread
|
||||
self.thread_last_update
|
||||
.insert(thread_result.thread_id, now);
|
||||
self.thread_last_update.insert(thread_result.thread_id, now);
|
||||
|
||||
// Update per-job hash rate as the sum of per-thread EMAs and publish
|
||||
let job_rate: f64 = self.thread_rate_ema.values().copied().sum();
|
||||
self.last_hash_rate = job_rate;
|
||||
metrics::set_job_hash_rate(self.engine_name, job_id, job_rate);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -393,6 +444,14 @@ impl MiningJob {
|
||||
self.completed_threads += 1;
|
||||
#[cfg(feature = "metrics")]
|
||||
{
|
||||
if let Some(job_id) = &self.job_id {
|
||||
// Remove this thread's hash rate series on completion
|
||||
metrics::remove_thread_hash_rate(
|
||||
self.engine_name,
|
||||
job_id,
|
||||
&thread_result.thread_id.to_string(),
|
||||
);
|
||||
}
|
||||
// Cleanup per-thread tracking on completion
|
||||
self.thread_last_update.remove(&thread_result.thread_id);
|
||||
self.thread_rate_ema.remove(&thread_result.thread_id);
|
||||
@@ -430,6 +489,18 @@ impl MiningJob {
|
||||
metrics::set_job_status_gauge(self.engine_name, job_id, "completed", 1);
|
||||
metrics::set_job_status_gauge(self.engine_name, job_id, "failed", 0);
|
||||
metrics::set_job_status_gauge(self.engine_name, job_id, "cancelled", 0);
|
||||
// Remove job hash rate on completion and clear per-thread series
|
||||
self.last_hash_rate = 0.0;
|
||||
metrics::remove_job_hash_rate(self.engine_name, job_id);
|
||||
for (tid, _) in self.thread_rate_ema.iter() {
|
||||
metrics::remove_thread_hash_rate(
|
||||
self.engine_name,
|
||||
job_id,
|
||||
&tid.to_string(),
|
||||
);
|
||||
}
|
||||
self.thread_last_update.clear();
|
||||
self.thread_rate_ema.clear();
|
||||
}
|
||||
}
|
||||
} else if self.completed_threads >= self.thread_handles.len()
|
||||
@@ -445,6 +516,18 @@ impl MiningJob {
|
||||
metrics::set_job_status_gauge(self.engine_name, job_id, "completed", 0);
|
||||
metrics::set_job_status_gauge(self.engine_name, job_id, "failed", 1);
|
||||
metrics::set_job_status_gauge(self.engine_name, job_id, "cancelled", 0);
|
||||
// Remove job hash rate on failure and clear per-thread series
|
||||
self.last_hash_rate = 0.0;
|
||||
metrics::remove_job_hash_rate(self.engine_name, job_id);
|
||||
for (tid, _) in self.thread_rate_ema.iter() {
|
||||
metrics::remove_thread_hash_rate(
|
||||
self.engine_name,
|
||||
job_id,
|
||||
&tid.to_string(),
|
||||
);
|
||||
}
|
||||
self.thread_last_update.clear();
|
||||
self.thread_rate_ema.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -461,6 +544,7 @@ fn mine_range_with_engine(
|
||||
range: EngineRange,
|
||||
cancel_flag: Arc<AtomicBool>,
|
||||
sender: Sender<ThreadResult>,
|
||||
progress_chunk_ms: u64,
|
||||
) {
|
||||
log::debug!(
|
||||
"Thread {} mining range {} to {} (inclusive)",
|
||||
@@ -469,44 +553,111 @@ fn mine_range_with_engine(
|
||||
range.end
|
||||
);
|
||||
|
||||
let status = engine.search_range(&ctx, range.clone(), &cancel_flag);
|
||||
// Chunk the range into subranges to emit periodic progress updates for metrics
|
||||
let mut current_start = range.start;
|
||||
let end = range.end;
|
||||
// Derive a rough chunk size from target milliseconds and an estimate of hashes/sec.
|
||||
// Start with a conservative default of 100k ops/sec per thread and scale by target ms.
|
||||
let target_ms = progress_chunk_ms;
|
||||
let est_ops_per_sec: u64 = 100_000;
|
||||
let derived_chunk: u64 = ((est_ops_per_sec.saturating_mul(target_ms)) / 1000).max(5_000);
|
||||
let chunk_size = U512::from(derived_chunk);
|
||||
let mut done = false;
|
||||
|
||||
let mut final_result = ThreadResult {
|
||||
thread_id,
|
||||
result: None,
|
||||
hash_count: 0,
|
||||
completed: true,
|
||||
};
|
||||
while current_start <= end && !cancel_flag.load(Ordering::Relaxed) {
|
||||
let mut current_end = current_start
|
||||
.saturating_add(chunk_size)
|
||||
.saturating_sub(U512::from(1u64));
|
||||
if current_end > end {
|
||||
current_end = end;
|
||||
}
|
||||
|
||||
match status {
|
||||
engine_cpu::EngineStatus::Found {
|
||||
candidate: EngineCandidate {
|
||||
nonce,
|
||||
work,
|
||||
distance,
|
||||
},
|
||||
hash_count,
|
||||
} => {
|
||||
final_result.result = Some(MiningJobResult {
|
||||
nonce,
|
||||
work,
|
||||
distance,
|
||||
});
|
||||
final_result.hash_count = hash_count;
|
||||
let sub_range = EngineRange {
|
||||
start: current_start,
|
||||
end: current_end,
|
||||
};
|
||||
|
||||
let status = engine.search_range(&ctx, sub_range.clone(), &cancel_flag);
|
||||
|
||||
match status {
|
||||
engine_cpu::EngineStatus::Found {
|
||||
candidate:
|
||||
EngineCandidate {
|
||||
nonce,
|
||||
work,
|
||||
distance,
|
||||
},
|
||||
hash_count,
|
||||
} => {
|
||||
// Send final result with found candidate and the hashes covered in this subrange
|
||||
let final_result = ThreadResult {
|
||||
thread_id,
|
||||
result: Some(MiningJobResult {
|
||||
nonce,
|
||||
work,
|
||||
distance,
|
||||
}),
|
||||
hash_count,
|
||||
completed: true,
|
||||
};
|
||||
if sender.send(final_result).is_err() {
|
||||
log::warn!("Thread {} failed to send final result", thread_id);
|
||||
}
|
||||
done = true;
|
||||
break;
|
||||
}
|
||||
engine_cpu::EngineStatus::Exhausted { hash_count } => {
|
||||
// Send intermediate progress update for this chunk
|
||||
let update = ThreadResult {
|
||||
thread_id,
|
||||
result: None,
|
||||
hash_count,
|
||||
completed: false,
|
||||
};
|
||||
if sender.send(update).is_err() {
|
||||
log::warn!("Thread {} failed to send progress update", thread_id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
engine_cpu::EngineStatus::Cancelled { hash_count } => {
|
||||
// Send last progress update and stop
|
||||
let update = ThreadResult {
|
||||
thread_id,
|
||||
result: None,
|
||||
hash_count,
|
||||
completed: false,
|
||||
};
|
||||
if sender.send(update).is_err() {
|
||||
log::warn!("Thread {} failed to send cancel update", thread_id);
|
||||
}
|
||||
done = true;
|
||||
break;
|
||||
}
|
||||
engine_cpu::EngineStatus::Running { .. } => {
|
||||
// Not expected from a synchronous engine chunk call; ignore
|
||||
}
|
||||
}
|
||||
engine_cpu::EngineStatus::Exhausted { hash_count } => {
|
||||
final_result.hash_count = hash_count;
|
||||
}
|
||||
engine_cpu::EngineStatus::Cancelled { hash_count } => {
|
||||
final_result.hash_count = hash_count;
|
||||
}
|
||||
engine_cpu::EngineStatus::Running { .. } => {
|
||||
// Should not happen for synchronous search_range; treat as completed without result.
|
||||
|
||||
if current_end == end {
|
||||
break;
|
||||
}
|
||||
current_start = current_end.saturating_add(U512::from(1u64));
|
||||
}
|
||||
|
||||
if sender.send(final_result).is_err() {
|
||||
log::warn!("Thread {} failed to send completion status", thread_id);
|
||||
if !done {
|
||||
// Signal thread completion with no final candidate
|
||||
let final_result = ThreadResult {
|
||||
thread_id,
|
||||
result: None,
|
||||
hash_count: 0,
|
||||
completed: true,
|
||||
};
|
||||
if sender.send(final_result).is_err() {
|
||||
log::warn!(
|
||||
"Thread {} failed to send completion status after chunked search",
|
||||
thread_id
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
log::debug!("Thread {} completed.", thread_id);
|
||||
@@ -556,6 +707,10 @@ pub async fn handle_mine_request(
|
||||
log::debug!("Received mine request: {:?}", request);
|
||||
if let Err(e) = validate_mining_request(&request) {
|
||||
log::warn!("Invalid mine request ({}): {}", request.job_id, e);
|
||||
#[cfg(feature = "metrics")]
|
||||
{
|
||||
metrics::inc_mine_requests("invalid");
|
||||
}
|
||||
return Ok(warp::reply::with_status(
|
||||
warp::reply::json(&MiningResponse {
|
||||
status: ApiResponseStatus::Error,
|
||||
@@ -579,6 +734,10 @@ pub async fn handle_mine_request(
|
||||
match state.add_job(request.job_id.clone(), job).await {
|
||||
Ok(_) => {
|
||||
log::debug!(target: "miner", "Accepted mine request for job ID: {}", request.job_id);
|
||||
#[cfg(feature = "metrics")]
|
||||
{
|
||||
metrics::inc_mine_requests("accepted");
|
||||
}
|
||||
Ok(warp::reply::with_status(
|
||||
warp::reply::json(&MiningResponse {
|
||||
status: ApiResponseStatus::Accepted,
|
||||
@@ -590,6 +749,15 @@ pub async fn handle_mine_request(
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Failed to add job {}: {}", request.job_id, e);
|
||||
#[cfg(feature = "metrics")]
|
||||
{
|
||||
let result = if e.contains("already") {
|
||||
"duplicate"
|
||||
} else {
|
||||
"error"
|
||||
};
|
||||
metrics::inc_mine_requests(result);
|
||||
}
|
||||
Ok(warp::reply::with_status(
|
||||
warp::reply::json(&MiningResponse {
|
||||
status: ApiResponseStatus::Error,
|
||||
@@ -627,7 +795,7 @@ pub async fn handle_result_request(
|
||||
}
|
||||
};
|
||||
|
||||
let api_status = match job.status {
|
||||
let status = match job.status {
|
||||
JobStatus::Running => ApiResponseStatus::Running,
|
||||
JobStatus::Completed => ApiResponseStatus::Completed,
|
||||
JobStatus::Failed => ApiResponseStatus::Failed,
|
||||
@@ -646,7 +814,7 @@ pub async fn handle_result_request(
|
||||
|
||||
Ok(warp::reply::with_status(
|
||||
warp::reply::json(&resonance_miner_api::MiningResult {
|
||||
status: api_status,
|
||||
status: status,
|
||||
job_id,
|
||||
nonce,
|
||||
work,
|
||||
@@ -738,7 +906,8 @@ pub async fn run(config: ServiceConfig) -> anyhow::Result<()> {
|
||||
};
|
||||
log::info!("Using engine: {}", engine.name());
|
||||
|
||||
let service = MiningService::new(num_cores, engine.clone());
|
||||
let progress_chunk_ms = config.progress_chunk_ms.unwrap_or(2000);
|
||||
let service = MiningService::new(num_cores, engine.clone(), progress_chunk_ms);
|
||||
|
||||
// Start mining loop
|
||||
service.start_mining_loop().await;
|
||||
|
||||
@@ -32,7 +32,11 @@
|
||||
}
|
||||
],
|
||||
"options": {
|
||||
"reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false },
|
||||
"reduceOptions": {
|
||||
"calcs": ["lastNotNull"],
|
||||
"fields": "",
|
||||
"values": false
|
||||
},
|
||||
"orientation": "auto",
|
||||
"colorMode": "value",
|
||||
"graphMode": "none",
|
||||
@@ -61,7 +65,11 @@
|
||||
}
|
||||
],
|
||||
"options": {
|
||||
"reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false },
|
||||
"reduceOptions": {
|
||||
"calcs": ["lastNotNull"],
|
||||
"fields": "",
|
||||
"values": false
|
||||
},
|
||||
"orientation": "horizontal",
|
||||
"colorMode": "value",
|
||||
"graphMode": "none",
|
||||
@@ -86,7 +94,11 @@
|
||||
}
|
||||
],
|
||||
"options": {
|
||||
"reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false },
|
||||
"reduceOptions": {
|
||||
"calcs": ["lastNotNull"],
|
||||
"fields": "",
|
||||
"values": false
|
||||
},
|
||||
"orientation": "auto",
|
||||
"colorMode": "value",
|
||||
"graphMode": "area",
|
||||
@@ -107,7 +119,7 @@
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
"expr": "sum by (engine) (miner_job_hash_rate{engine=~\"$engine\"})",
|
||||
"expr": "sum by (engine) (max_over_time(miner_job_hash_rate{engine=~\"$engine\"}[1m]))",
|
||||
"legendFormat": "{{engine}}"
|
||||
}
|
||||
],
|
||||
@@ -128,7 +140,8 @@
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
"expr": "sum by (engine, status) (miner_jobs_by_engine_total{engine=~\"$engine\"})"
|
||||
"expr": "sum by (engine, status) (increase(miner_jobs_by_engine_total{engine=~\"$engine\"}[1m]))",
|
||||
"legendFormat": "{{engine}}, {{status}}"
|
||||
}
|
||||
],
|
||||
"fieldConfig": {
|
||||
@@ -138,7 +151,11 @@
|
||||
"options": {
|
||||
"displayMode": "gradient",
|
||||
"orientation": "horizontal",
|
||||
"reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false },
|
||||
"reduceOptions": {
|
||||
"calcs": ["lastNotNull"],
|
||||
"fields": "",
|
||||
"values": false
|
||||
},
|
||||
"showUnfilled": true
|
||||
}
|
||||
},
|
||||
@@ -151,7 +168,7 @@
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
"expr": "sum by (engine) (increase(miner_job_hashes_total{engine=~\"$engine\"}[$__rate_interval]))",
|
||||
"expr": "sum by (engine) (increase(miner_job_hashes_total{engine=~\"$engine\"}[1m]))",
|
||||
"legendFormat": "{{engine}}"
|
||||
}
|
||||
],
|
||||
@@ -194,7 +211,8 @@
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
"expr": "topk(10, miner_job_hash_rate{engine=~\"$engine\"})"
|
||||
"expr": "topk(10, miner_job_hash_rate{engine=~\"$engine\"})",
|
||||
"legendFormat": "{{engine}}, {{job_id}}"
|
||||
}
|
||||
],
|
||||
"fieldConfig": {
|
||||
@@ -219,7 +237,8 @@
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
"expr": "sum by (engine) (increase(miner_job_hashes_total{engine=~\"$engine\"}[5m]))"
|
||||
"expr": "sum by (engine) (increase(miner_job_hashes_total{engine=~\"$engine\"}[5m]))",
|
||||
"legendFormat": "{{engine}}"
|
||||
}
|
||||
],
|
||||
"fieldConfig": {
|
||||
@@ -245,8 +264,8 @@
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
"expr": "miner_job_hash_rate{engine=~\"$engine\", job_id=~\"$job_id\"}",
|
||||
"legendFormat": "{{engine}} | {{job_id}}"
|
||||
"expr": "max_over_time(miner_job_hash_rate{engine=~\"$engine\", job_id=~\"$job_id\"}[1m])",
|
||||
"legendFormat": "{{engine}}, {{job_id}}"
|
||||
}
|
||||
],
|
||||
"fieldConfig": {
|
||||
@@ -267,7 +286,8 @@
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
"expr": "miner_thread_hash_rate{engine=~\"$engine\", job_id=~\"$job_id\", thread_id=~\"$thread_id\"}"
|
||||
"expr": "max_over_time(miner_thread_hash_rate{engine=~\"$engine\", job_id=~\"$job_id\", thread_id=~\"$thread_id\"}[1m])",
|
||||
"legendFormat": "{{engine}}, {{job_id}}, {{thread_id}}"
|
||||
}
|
||||
],
|
||||
"fieldConfig": {
|
||||
|
||||
@@ -0,0 +1,416 @@
|
||||
{
|
||||
"annotations": {
|
||||
"list": [
|
||||
{
|
||||
"builtIn": 1,
|
||||
"datasource": "-- Grafana --",
|
||||
"enable": true,
|
||||
"hide": true,
|
||||
"iconColor": "rgba(0, 211, 255, 1)",
|
||||
"name": "Annotations & Alerts",
|
||||
"type": "dashboard"
|
||||
}
|
||||
]
|
||||
},
|
||||
"editable": true,
|
||||
"fiscalYearStartMonth": 0,
|
||||
"graphTooltip": 0,
|
||||
"id": null,
|
||||
"links": [],
|
||||
"liveNow": false,
|
||||
"panels": [
|
||||
{
|
||||
"type": "stat",
|
||||
"title": "24h Peak Hash Rate (selected instances)",
|
||||
"description": "Sum of 24h max per-instance job hash rate across selected instances (trend-friendly)",
|
||||
"gridPos": { "h": 5, "w": 8, "x": 0, "y": 0 },
|
||||
"datasource": "$DS_PROMETHEUS",
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
"expr": "sum by (instance) (max_over_time(miner_job_hash_rate{job=~\"$job\", instance=~\"$instance\"}[24h:1m]))"
|
||||
}
|
||||
],
|
||||
"options": {
|
||||
"reduceOptions": {
|
||||
"calcs": ["lastNotNull"],
|
||||
"fields": "",
|
||||
"values": false
|
||||
},
|
||||
"orientation": "auto",
|
||||
"colorMode": "value",
|
||||
"graphMode": "none",
|
||||
"justifyMode": "auto",
|
||||
"textMode": "value"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": { "unit": "ops" },
|
||||
"overrides": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "timeseries",
|
||||
"title": "Global Hash Rate by Instance",
|
||||
"description": "sum of per-job hash rate grouped by instance (1m max window for trend)",
|
||||
"gridPos": { "h": 8, "w": 16, "x": 8, "y": 0 },
|
||||
"datasource": "$DS_PROMETHEUS",
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
"expr": "sum by (instance) (max_over_time(miner_job_hash_rate{job=~\"$job\", instance=~\"$instance\"}[1m]))",
|
||||
"legendFormat": "{{instance}}"
|
||||
}
|
||||
],
|
||||
"fieldConfig": {
|
||||
"defaults": { "unit": "ops" },
|
||||
"overrides": []
|
||||
},
|
||||
"options": {
|
||||
"legend": { "displayMode": "list", "placement": "bottom" },
|
||||
"tooltip": { "mode": "multi", "sort": "none" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "table",
|
||||
"title": "Per-Instance Hash Rate (current vs 1h peak)",
|
||||
"description": "Current (1m max) and 1h peak per instance (filter by engine)",
|
||||
"gridPos": { "h": 9, "w": 8, "x": 0, "y": 5 },
|
||||
"datasource": "$DS_PROMETHEUS",
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
"expr": "sum by (instance) (max_over_time(miner_job_hash_rate{job=~\"$job\", instance=~\"$instance\", engine=~\"$engine\"}[1m]))",
|
||||
"legendFormat": "current"
|
||||
},
|
||||
{
|
||||
"refId": "B",
|
||||
"expr": "sum by (instance) (max_over_time(miner_job_hash_rate{job=~\"$job\", instance=~\"$instance\", engine=~\"$engine\"}[1h:1m]))",
|
||||
"legendFormat": "peak_1h"
|
||||
}
|
||||
],
|
||||
"transformations": [
|
||||
{
|
||||
"id": "merge",
|
||||
"options": {}
|
||||
},
|
||||
{
|
||||
"id": "groupBy",
|
||||
"options": {
|
||||
"fields": {},
|
||||
"aggregations": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "organize",
|
||||
"options": {
|
||||
"excludeByName": {
|
||||
"Time 1": true,
|
||||
"Time 2": true
|
||||
},
|
||||
"indexByName": {
|
||||
"instance": 0,
|
||||
"A": 1,
|
||||
"B": 2
|
||||
},
|
||||
"renameByName": {
|
||||
"A": "current_ops",
|
||||
"B": "peak_1h_ops"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "calculateField",
|
||||
"options": {
|
||||
"mode": "binary",
|
||||
"binary": {
|
||||
"reducer": "last",
|
||||
"left": "A",
|
||||
"operator": "/",
|
||||
"right": "B",
|
||||
"replaceFields": false
|
||||
},
|
||||
"reduce": {
|
||||
"reducer": "last"
|
||||
},
|
||||
"alias": "delta_current_over_peak"
|
||||
}
|
||||
}
|
||||
],
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"custom": { "align": "auto" },
|
||||
"unit": "ops",
|
||||
"decimals": 2,
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{ "color": "green", "value": null },
|
||||
{ "color": "orange", "value": 0.5 },
|
||||
{ "color": "red", "value": 0.8 }
|
||||
]
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"options": {
|
||||
"showHeader": true,
|
||||
"footer": { "show": false },
|
||||
"frameIndex": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "timeseries",
|
||||
"title": "Per-Instance Job Hash Rate (over time)",
|
||||
"description": "sum(max_over_time(miner_job_hash_rate[1m])) grouped by instance (filter by engine) for trend",
|
||||
"gridPos": { "h": 8, "w": 16, "x": 8, "y": 8 },
|
||||
"datasource": "$DS_PROMETHEUS",
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
"expr": "sum by (instance) (max_over_time(miner_job_hash_rate{job=~\"$job\", instance=~\"$instance\", engine=~\"$engine\"}[1m]))",
|
||||
"legendFormat": "{{instance}}"
|
||||
}
|
||||
],
|
||||
"fieldConfig": {
|
||||
"defaults": { "unit": "ops" },
|
||||
"overrides": []
|
||||
},
|
||||
"options": {
|
||||
"legend": { "displayMode": "list", "placement": "bottom" },
|
||||
"tooltip": { "mode": "multi", "sort": "none" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "timeseries",
|
||||
"title": "Average Per-Thread Hash Rate by Instance",
|
||||
"description": "avg(max_over_time(miner_thread_hash_rate[1m])) grouped by instance (EMA-smoothed; filter by engine)",
|
||||
"gridPos": { "h": 8, "w": 24, "x": 0, "y": 16 },
|
||||
"datasource": "$DS_PROMETHEUS",
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
"expr": "sum by (instance) (max_over_time(miner_job_hash_rate{job=~\"$job\", instance=~\"$instance\", engine=~\"$engine\"}[1m]))",
|
||||
"legendFormat": "{{instance}}"
|
||||
}
|
||||
],
|
||||
"fieldConfig": {
|
||||
"defaults": { "unit": "ops" },
|
||||
"overrides": []
|
||||
},
|
||||
"options": {
|
||||
"legend": { "displayMode": "list", "placement": "bottom" },
|
||||
"tooltip": { "mode": "multi", "sort": "none" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "table",
|
||||
"title": "Top 15 Jobs by Instance (filter by engine)",
|
||||
"gridPos": { "h": 9, "w": 12, "x": 0, "y": 24 },
|
||||
"datasource": "$DS_PROMETHEUS",
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
"expr": "topk(15, max_over_time(miner_job_hash_rate{job=~\"$job\", instance=~\"$instance\", engine=~\"$engine\"}[1m]))",
|
||||
"legendFormat": "{{instance}}, {{engine}}, {{job_id}}"
|
||||
}
|
||||
],
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"custom": { "align": "auto" },
|
||||
"unit": "ops",
|
||||
"decimals": 2
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"options": {
|
||||
"showHeader": true,
|
||||
"footer": { "show": false },
|
||||
"frameIndex": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "table",
|
||||
"title": "Per-Instance Total Hashes (1m increase; filter by engine)",
|
||||
"gridPos": { "h": 9, "w": 12, "x": 12, "y": 24 },
|
||||
"datasource": "$DS_PROMETHEUS",
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
"expr": "sum by (instance) (increase(miner_job_hashes_total{job=~\"$job\", instance=~\"$instance\", engine=~\"$engine\"}[1m]))",
|
||||
"legendFormat": "{{instance}}"
|
||||
}
|
||||
],
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"custom": { "align": "auto" },
|
||||
"unit": "ops",
|
||||
"decimals": 0
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"options": {
|
||||
"showHeader": true,
|
||||
"footer": { "show": false },
|
||||
"frameIndex": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "timeseries",
|
||||
"title": "Per-Instance Per-Engine Job Hash Rate",
|
||||
"description": "sum(max_over_time(miner_job_hash_rate[1m])) grouped by instance and engine",
|
||||
"gridPos": { "h": 9, "w": 24, "x": 0, "y": 33 },
|
||||
"datasource": "$DS_PROMETHEUS",
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
"expr": "sum by (instance, engine) (max_over_time(miner_job_hash_rate{job=~\"$job\", instance=~\"$instance\", engine=~\"$engine\"}[1m]))",
|
||||
"legendFormat": "{{instance}}, {{engine}}"
|
||||
}
|
||||
],
|
||||
"fieldConfig": {
|
||||
"defaults": { "unit": "ops" },
|
||||
"overrides": []
|
||||
},
|
||||
"options": {
|
||||
"legend": { "displayMode": "list", "placement": "bottom" },
|
||||
"tooltip": { "mode": "multi", "sort": "none" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "stat",
|
||||
"title": "Active Jobs (by instance)",
|
||||
"description": "Number of currently running mining jobs on each instance",
|
||||
"gridPos": { "h": 5, "w": 6, "x": 0, "y": 42 },
|
||||
"datasource": "$DS_PROMETHEUS",
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
"expr": "sum by (instance) (miner_active_jobs{job=~\"$job\", instance=~\"$instance\"})"
|
||||
}
|
||||
],
|
||||
"options": {
|
||||
"reduceOptions": {
|
||||
"calcs": ["lastNotNull"],
|
||||
"fields": "",
|
||||
"values": false
|
||||
},
|
||||
"orientation": "auto",
|
||||
"colorMode": "value",
|
||||
"graphMode": "none",
|
||||
"justifyMode": "auto",
|
||||
"textMode": "value"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": { "unit": "short" },
|
||||
"overrides": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "table",
|
||||
"title": "Mine Requests (5m increase; by instance & result)",
|
||||
"description": "Accepted, duplicate, invalid, error counts",
|
||||
"gridPos": { "h": 9, "w": 18, "x": 6, "y": 42 },
|
||||
"datasource": "$DS_PROMETHEUS",
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
"expr": "sum by (instance, result) (increase(miner_mine_requests_total{job=~\"$job\", instance=~\"$instance\"}[5m]))",
|
||||
"legendFormat": "{{instance}}, {{result}}"
|
||||
}
|
||||
],
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"custom": { "align": "auto" },
|
||||
"unit": "short",
|
||||
"decimals": 0
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"options": {
|
||||
"showHeader": true,
|
||||
"footer": { "show": false },
|
||||
"frameIndex": 0
|
||||
}
|
||||
}
|
||||
],
|
||||
"refresh": "10s",
|
||||
"schemaVersion": 38,
|
||||
"style": "dark",
|
||||
"tags": ["quantus", "miner", "prometheus", "instances"],
|
||||
"templating": {
|
||||
"list": [
|
||||
{
|
||||
"type": "datasource",
|
||||
"name": "DS_PROMETHEUS",
|
||||
"hide": 0,
|
||||
"query": "prometheus",
|
||||
"current": {},
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
"type": "query",
|
||||
"name": "instance",
|
||||
"label": "Instance(s)",
|
||||
"hide": 0,
|
||||
"datasource": "$DS_PROMETHEUS",
|
||||
"definition": "label_values(miner_job_hash_rate, instance)",
|
||||
"includeAll": true,
|
||||
"multi": true,
|
||||
"refresh": 2,
|
||||
"sort": 1,
|
||||
"query": "label_values(miner_job_hash_rate, instance)",
|
||||
"regex": "",
|
||||
"current": {
|
||||
"selected": false,
|
||||
"text": ["All"],
|
||||
"value": ["$__all"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "query",
|
||||
"name": "engine",
|
||||
"label": "Engine(s)",
|
||||
"hide": 0,
|
||||
"datasource": "$DS_PROMETHEUS",
|
||||
"definition": "label_values(miner_job_hash_rate, engine)",
|
||||
"includeAll": true,
|
||||
"multi": true,
|
||||
"refresh": 2,
|
||||
"sort": 1,
|
||||
"query": "label_values(miner_job_hash_rate, engine)",
|
||||
"regex": "",
|
||||
"current": {
|
||||
"selected": false,
|
||||
"text": ["All"],
|
||||
"value": ["$__all"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "query",
|
||||
"name": "job",
|
||||
"label": "Job(s)",
|
||||
"hide": 0,
|
||||
"datasource": "$DS_PROMETHEUS",
|
||||
"definition": "label_values(miner_job_hash_rate, job)",
|
||||
"includeAll": true,
|
||||
"multi": true,
|
||||
"refresh": 2,
|
||||
"sort": 1,
|
||||
"query": "label_values(miner_job_hash_rate, job)",
|
||||
"regex": "",
|
||||
"current": {
|
||||
"selected": false,
|
||||
"text": ["All"],
|
||||
"value": ["$__all"]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"time": { "from": "now-6h", "to": "now" },
|
||||
"timepicker": {},
|
||||
"timezone": "",
|
||||
"title": "Quantus Miner - Instances Comparison",
|
||||
"version": 1,
|
||||
"uid": null
|
||||
}
|
||||
@@ -32,7 +32,11 @@
|
||||
}
|
||||
],
|
||||
"options": {
|
||||
"reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false },
|
||||
"reduceOptions": {
|
||||
"calcs": ["lastNotNull"],
|
||||
"fields": "",
|
||||
"values": false
|
||||
},
|
||||
"orientation": "auto",
|
||||
"colorMode": "value",
|
||||
"graphMode": "none",
|
||||
@@ -52,7 +56,7 @@
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
"expr": "sum(miner_hash_rate)",
|
||||
"expr": "sum(max_over_time(miner_job_hash_rate[1m]))",
|
||||
"legendFormat": "global"
|
||||
}
|
||||
],
|
||||
@@ -73,7 +77,8 @@
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
"expr": "sum by (status) (miner_jobs_total)"
|
||||
"expr": "sum by (status) (miner_jobs_total)",
|
||||
"legendFormat": "{{status}}"
|
||||
}
|
||||
],
|
||||
"fieldConfig": {
|
||||
@@ -83,7 +88,11 @@
|
||||
"options": {
|
||||
"displayMode": "gradient",
|
||||
"orientation": "horizontal",
|
||||
"reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false },
|
||||
"reduceOptions": {
|
||||
"calcs": ["lastNotNull"],
|
||||
"fields": "",
|
||||
"values": false
|
||||
},
|
||||
"showUnfilled": true
|
||||
}
|
||||
},
|
||||
@@ -95,7 +104,8 @@
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
"expr": "sum by (engine, status) (miner_jobs_by_engine_total)"
|
||||
"expr": "sum by (engine, status) (increase(miner_jobs_by_engine_total[1m]))",
|
||||
"legendFormat": "{{engine}}, {{status}}"
|
||||
}
|
||||
],
|
||||
"fieldConfig": {
|
||||
@@ -105,7 +115,11 @@
|
||||
"options": {
|
||||
"displayMode": "gradient",
|
||||
"orientation": "horizontal",
|
||||
"reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false },
|
||||
"reduceOptions": {
|
||||
"calcs": ["lastNotNull"],
|
||||
"fields": "",
|
||||
"values": false
|
||||
},
|
||||
"showUnfilled": true
|
||||
}
|
||||
},
|
||||
@@ -139,8 +153,8 @@
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
"expr": "miner_thread_hash_rate{engine=~\"$engine\", job_id=~\"$job_id\", thread_id=~\"$thread_id\"}",
|
||||
"legendFormat": "{{engine}} | job={{job_id}} | th={{thread_id}}"
|
||||
"expr": "max_over_time(miner_thread_hash_rate{engine=~\"$engine\", job_id=~\"$job_id\", thread_id=~\"$thread_id\"}[1m])",
|
||||
"legendFormat": "{{engine}}, {{job_id}}, {{thread_id}}"
|
||||
}
|
||||
],
|
||||
"fieldConfig": {
|
||||
@@ -160,11 +174,15 @@
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
"expr": "increase(miner_hashes_total[5m])"
|
||||
"expr": "increase(miner_hashes_total[1m])"
|
||||
}
|
||||
],
|
||||
"options": {
|
||||
"reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false },
|
||||
"reduceOptions": {
|
||||
"calcs": ["lastNotNull"],
|
||||
"fields": "",
|
||||
"values": false
|
||||
},
|
||||
"orientation": "auto",
|
||||
"colorMode": "value",
|
||||
"graphMode": "none",
|
||||
@@ -184,7 +202,8 @@
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
"expr": "sum by (engine, job_id) (increase(miner_job_hashes_total{engine=~\"$engine\"}[5m]))"
|
||||
"expr": "sum by (engine, job_id) (increase(miner_job_hashes_total{engine=~\"$engine\"}[1m]))",
|
||||
"legendFormat": "{{engine}}, {{job_id}}"
|
||||
}
|
||||
],
|
||||
"fieldConfig": {
|
||||
@@ -206,7 +225,7 @@
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
"expr": "miner_job_status{engine=~\"$engine\", job_id=~\"$job_id\"}",
|
||||
"expr": "max_over_time(miner_job_status{engine=~\"$engine\", job_id=~\"$job_id\"}[1m])",
|
||||
"legendFormat": "{{job_id}}:{{status}}"
|
||||
}
|
||||
],
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
# 04 — Metrics Freshness, Chunking Knob, Remove-on-End, and Dashboards
|
||||
|
||||
This document summarizes the prompts and responses since the last update (03), focused on making the miner’s metrics accurate, timely, and useful in Grafana for engine comparability and per-instance analysis.
|
||||
|
||||
Scope covered:
|
||||
- Fixing miner hash-rate metrics (job/global) so they reflect active work and show trend meaningfully
|
||||
- Emitting periodic progress updates (chunking) and adding a time-based chunking knob
|
||||
- Switching to remove-on-end for rate gauges to avoid scrape-timing artifacts
|
||||
- Extending dashboards with cleaner labels, trend queries, 24h/1h peaks, current vs peak visuals, and a job filter
|
||||
- Adding service-level metrics to distinguish “idle because no jobs” vs “actively mining”
|
||||
- Prometheus scrape interval guidance
|
||||
|
||||
---
|
||||
|
||||
## 1) What we observed
|
||||
|
||||
- On resonance (busy), job/global hash rates appeared; on heisenberg (quiet), they often showed 0.
|
||||
- miner_thread_hash_rate was present (thanks to per-thread EMA), but miner_job_hash_rate/miner_hash_rate often flatlined at 0.
|
||||
- “Instances Comparison” dashboard had labels rendered as Prometheus label objects (e.g., `{instance="a1.i.res.fm"}`) and some panels were not informative.
|
||||
- We needed trend-oriented panels for engine comparability, not just instantaneous gauges.
|
||||
|
||||
Root causes:
|
||||
- Engines reported results only at the end of large ranges, so totals/elapsed were stale most of the time.
|
||||
- Job/global rates were computed as totals/elapsed and reset aggressively on job completion, leading to zeros or absent data.
|
||||
- Thread EMA rates weren’t cleared on completion — this made thread charts look alive even when jobs weren’t running.
|
||||
- Some dashboards summed the “instant” miner_hash_rate instead of a trend-friendly aggregate.
|
||||
|
||||
---
|
||||
|
||||
## 2) Decisions
|
||||
|
||||
- Emit periodic sub-range progress from each worker (chunking) for real-time metrics.
|
||||
- Compute per-thread delta-rate (hash_count / Δt) and smooth with EMA; derive per-job and global rates from the sum of thread EMAs.
|
||||
- Switch from “write zero on end” to “remove-on-end” for rate series to avoid scrape-timing artifacts and produce clean rollups.
|
||||
- Add a time-based knob for progress chunking: `--progress-chunk-ms` (`MINER_PROGRESS_CHUNK_MS`).
|
||||
- Adopt trend-oriented queries in Grafana (max_over_time/increase) to show recent performance (current vs. 1h peak, etc.).
|
||||
- Add service-level metrics:
|
||||
- `miner_active_jobs` (gauge)
|
||||
- `miner_mine_requests_total{result}` (counter: accepted, duplicate, invalid, error)
|
||||
- Add a job filter to dashboards to select a specific scrape job per chain (e.g., heisenberg vs. resonance).
|
||||
|
||||
---
|
||||
|
||||
## 3) Implementation highlights
|
||||
|
||||
A) Periodic progress updates (chunking)
|
||||
- The mining loop (`mine_range_with_engine`) now splits the assigned range into sub-ranges (“chunks”).
|
||||
- After each chunk, a `ThreadResult` is emitted with `hash_count` even if no solution is found.
|
||||
- Default chunk size is derived from `--progress-chunk-ms`:
|
||||
- `derived_chunk = max(5_000, est_ops_per_sec * progress_chunk_ms / 1000)`, with `est_ops_per_sec = 100_000` as a conservative starting point.
|
||||
- This gives a steady cadence of per-thread updates and fresher job/global rates.
|
||||
|
||||
B) Rates derived from thread EMAs
|
||||
- For each `ThreadResult`, compute instantaneous rate = `hash_count / Δt`, then smooth with EMA (`THREAD_RATE_EMA_ALPHA = 0.2`).
|
||||
- Publish `miner_thread_hash_rate{engine,job_id,thread_id}` = EMA.
|
||||
- Per-job rate = sum of thread EMAs per job → `miner_job_hash_rate{engine,job_id}`.
|
||||
- Global rate = sum of per-job rates across running jobs → `miner_hash_rate`.
|
||||
|
||||
C) Remove-on-end semantics
|
||||
- When a thread ends or a job transitions to Completed/Failed/Cancelled:
|
||||
- Remove thread rate series: `remove_thread_hash_rate(engine, job_id, thread_id)`.
|
||||
- Remove job rate series: `remove_job_hash_rate(engine, job_id)`.
|
||||
- Rationale: this avoids scrape-race zero spikes and produces cleaner trend rollups.
|
||||
|
||||
D) Metrics added
|
||||
- `miner_active_jobs` (gauge)
|
||||
- Increment on job accept, decrement on job cancel/remove. Reflects #running jobs “now”.
|
||||
- `miner_mine_requests_total{result}` (counter)
|
||||
- Increment per /mine request outcome: `accepted`, `duplicate`, `invalid`, `error`.
|
||||
- Effect: makes it obvious when a miner is idle because the node isn’t issuing /mine (active_jobs = 0, requests not increasing) vs actively mining.
|
||||
|
||||
E) CLI knob for chunking
|
||||
- `--progress-chunk-ms` (env: `MINER_PROGRESS_CHUNK_MS`) sets target milliseconds for per-thread progress updates.
|
||||
- Default: 2000 ms (if not provided).
|
||||
- Use ~1000–2000 ms with `scrape_interval: 2s` for responsive charts.
|
||||
|
||||
---
|
||||
|
||||
## 4) Prometheus/Grafana updates
|
||||
|
||||
A) Scrape interval guidance
|
||||
- Global: keep 15s.
|
||||
- Miner jobs: override to 2s (or 1s for short experiments; 5s to reduce Prometheus load).
|
||||
- Example per-job override:
|
||||
```
|
||||
scrape_configs:
|
||||
- job_name: 'quantus-miner-resonance'
|
||||
scrape_interval: 2s
|
||||
scrape_timeout: 1.5s
|
||||
scheme: https
|
||||
metrics_path: /metrics/miner
|
||||
static_configs:
|
||||
- targets: [a1.i.res.fm, a2.i.res.fm, a3.i.res.fm]
|
||||
- job_name: 'quantus-miner-heisenberg'
|
||||
scrape_interval: 2s
|
||||
scrape_timeout: 1.5s
|
||||
scheme: https
|
||||
metrics_path: /metrics/miner
|
||||
static_configs:
|
||||
- targets: [a1.t.res.fm, a2.t.res.fm, a3.t.res.fm]
|
||||
```
|
||||
|
||||
B) Dashboards
|
||||
- Three JSONs under `docs/grafana/`:
|
||||
- `miner-dashboard.json` (operator overview)
|
||||
- `engines-comparison-dashboard.json` (baseline vs fast comparison)
|
||||
- `instances-comparison-dashboard.json` (per-instance analysis)
|
||||
- Label display cleanup:
|
||||
- Legends use `{{engine}}, {{job_id}}, {{thread_id}}`, etc. (no `{label="value"}` surfacing).
|
||||
- Trend-oriented queries:
|
||||
- Use `max_over_time(...[1m])` for rates and `increase(...[1m])` for counters so panels show recent performance rather than just instantaneous snapshots.
|
||||
- Peak vs Current visuals:
|
||||
- Instances comparison now includes:
|
||||
- A 24h peak stat (computed via PromQL subqueries: `[24h:1m]`) for trend-friendly peak potential.
|
||||
- A per-instance table (current vs 1h peak + delta), with filters for job/instance/engine:
|
||||
- Current: `sum by (instance) (max_over_time(miner_job_hash_rate{...}[1m]))`
|
||||
- Peak 1h: `sum by (instance) (max_over_time(miner_job_hash_rate{...}[1h:1m]))`
|
||||
- Delta column: `current / peak_1h`
|
||||
- New panels:
|
||||
- Active Jobs by instance: `miner_active_jobs`
|
||||
- Mine Requests 5m increase by instance & result: `increase(miner_mine_requests_total{...}[5m])`
|
||||
|
||||
---
|
||||
|
||||
## 5) Operational guidance
|
||||
|
||||
- Scrape:
|
||||
- Per-job: 2s scrape interval (1s for deep dives).
|
||||
- Chunking:
|
||||
- Start with `--progress-chunk-ms 2000` (or 1000 for more frequent updates).
|
||||
- Engines:
|
||||
- Compare `cpu-baseline` vs `cpu-fast` by setting different instances to different engines and using the Engines Comparison dashboard.
|
||||
- Trend windows:
|
||||
- Dashboards use `[1m]` windows for charts to reduce “zero gaps” while preserving short-time responsiveness.
|
||||
- Peak windows can be adjusted (e.g., `[6h:1m]` or `[24h:1m]`) depending on what “recent performance” means for your use case.
|
||||
|
||||
---
|
||||
|
||||
## 6) Next steps
|
||||
|
||||
- Auto-tune chunk size:
|
||||
- Estimate per-thread throughput in the first few chunks and adapt `derived_chunk` dynamically rather than relying on the static `est_ops_per_sec`.
|
||||
- Better per-thread trend smoothing:
|
||||
- Expose alpha as a CLI/env knob if operators want to tune EMA smoothing.
|
||||
- Dashboard polish:
|
||||
- Add a percent unit & color thresholds for the delta column; optionally include sparkline mini-charts.
|
||||
- Alerts/recording rules:
|
||||
- Record per-engine/per-instance hash rate aggregates and set basic alerts (e.g., “no jobs for X minutes” or “hash rate below expected baseline for Y minutes”).
|
||||
|
||||
---
|
||||
|
||||
## 7) Quick reference
|
||||
|
||||
- CLI:
|
||||
- `--engine cpu-baseline|cpu-fast`
|
||||
- `--progress-chunk-ms <ms>`
|
||||
- `--metrics-port <port>`
|
||||
- `--cores <n>` (alias: `--num-cores`)
|
||||
- Service metrics added:
|
||||
- `miner_active_jobs` (gauge)
|
||||
- `miner_mine_requests_total{result}` (counter)
|
||||
- Remove-on-end:
|
||||
- Thread/job rate gauges removed (series deleted) when completed/failed/cancelled.
|
||||
- Grafana JSONs:
|
||||
- `docs/grafana/miner-dashboard.json`
|
||||
- `docs/grafana/engines-comparison-dashboard.json`
|
||||
- `docs/grafana/instances-comparison-dashboard.json`
|
||||
|
||||
These changes make the miner’s metrics fresher, more accurate, and easy to compare across engines and instances, while avoiding scrape artifacts and providing the context needed to explain “0 ops/s” situations on quiet networks.
|
||||
@@ -0,0 +1,165 @@
|
||||
# 04 — Response Summary: Metrics Freshness, Chunking Knob, Remove-on-End, and Dashboards
|
||||
|
||||
This document summarises what was implemented since iteration 03 to improve metric accuracy, responsiveness, and usability for engine/instance comparisons.
|
||||
|
||||
## Overview
|
||||
|
||||
We addressed four main areas:
|
||||
1. Metrics freshness: emit periodic progress updates from workers so job/global hash rates update in near‑real time.
|
||||
2. Accuracy and artifact reduction: switch to remove‑on‑end for rate series (instead of writing zeros) to avoid scrape-timing spikes.
|
||||
3. Operator control: add a time‑based chunking knob to tune the heartbeat cadence without code changes.
|
||||
4. Observability and UX: extend Prometheus metrics and update Grafana dashboards to show meaningful trends and to disambiguate “idle because no jobs” vs “actively mining”.
|
||||
|
||||
---
|
||||
|
||||
## Code Changes
|
||||
|
||||
### 1) Periodic progress updates (chunking)
|
||||
|
||||
- Mining worker now splits its assigned nonce range into time‑targeted “chunks.”
|
||||
- After each chunk completes, the worker sends a `ThreadResult` with `hash_count` even if it didn’t find a solution.
|
||||
- Default chunk size derived from a time target:
|
||||
- `derived_chunk ≈ max(5_000, est_ops_per_sec * progress_chunk_ms / 1000)`
|
||||
- Using a conservative `est_ops_per_sec = 100_000` as an initial constant.
|
||||
- Effect: Prometheus/Grafana receive frequent progress updates so per‑job/global rates are responsive while work is ongoing.
|
||||
|
||||
### 2) Per-thread delta-rate with EMA smoothing, per‑job/global derived from threads
|
||||
|
||||
- On each `ThreadResult`, compute per‑thread instantaneous rate:
|
||||
- `instant_rate = hash_count / Δt`, then smooth:
|
||||
- `ema = alpha * instant_rate + (1 - alpha) * prev_ema` with `THREAD_RATE_EMA_ALPHA = 0.2`.
|
||||
- Publish `miner_thread_hash_rate{engine,job_id,thread_id} = ema` (gauge).
|
||||
- Per‑job rate = sum of thread EMAs → `miner_job_hash_rate{engine,job_id}` (gauge).
|
||||
- Global rate = sum of per‑job rates across running jobs → `miner_hash_rate` (gauge).
|
||||
|
||||
### 3) Remove-on-end for rate series
|
||||
|
||||
- When a thread completes or a job transitions to Completed/Failed/Cancelled:
|
||||
- Remove the corresponding gauge series instead of writing zeros:
|
||||
- `remove_thread_hash_rate(engine, job_id, thread_id)`
|
||||
- `remove_job_hash_rate(engine, job_id)`
|
||||
- Rationale: avoids scrape timing artifacts (spurious zeros) and yields cleaner trend rollups with range functions.
|
||||
|
||||
### 4) Time‑based chunking knob
|
||||
|
||||
- CLI/env:
|
||||
- `--progress-chunk-ms <ms>` (env: `MINER_PROGRESS_CHUNK_MS`)
|
||||
- Default (if omitted): 2000ms
|
||||
- Use ~1000–2000ms at a 2s Prometheus scrape interval for smooth charts and low overhead.
|
||||
|
||||
### 5) Service-level metrics
|
||||
|
||||
New Prometheus series:
|
||||
- `miner_active_jobs` (gauge): number of currently running jobs in the miner.
|
||||
- `miner_mine_requests_total{result}` (counter): counts `/mine` requests by `result`:
|
||||
- `accepted`, `duplicate`, `invalid`, `error`.
|
||||
|
||||
Wiring:
|
||||
- On job accept: `active_jobs + 1`, `mine_requests_total{result="accepted"} + 1`
|
||||
- On job cancel/remove: `active_jobs - 1`
|
||||
- On invalid request: `mine_requests_total{result="invalid"} + 1`
|
||||
- On duplicate or other add-job error: `result="duplicate"` or `result="error"` accordingly.
|
||||
|
||||
---
|
||||
|
||||
## Dashboard Updates (Grafana JSONs in `docs/grafana/`)
|
||||
|
||||
We maintained a clean, value‑only label style (e.g., `a1.i.res.fm`, `cpu-baseline`, `job_id`, `thread_id`), and adopted short range window functions to show trends rather than purely instantaneous views.
|
||||
|
||||
### 1) Miner Overview — `miner-dashboard.json`
|
||||
|
||||
- Global Hash Rate (over time):
|
||||
- `sum(max_over_time(miner_job_hash_rate[1m]))`
|
||||
- Jobs by Engine and Status:
|
||||
- `sum by (engine, status) (increase(miner_jobs_by_engine_total[1m]))`
|
||||
- Thread Hash Rate:
|
||||
- `max_over_time(miner_thread_hash_rate[1m])`
|
||||
- Total Hashes / Per-Job Hashes:
|
||||
- `increase(...[1m])`
|
||||
- Job Status:
|
||||
- `max_over_time(miner_job_status[1m])`
|
||||
|
||||
### 2) Engines Comparison — `engines-comparison-dashboard.json`
|
||||
|
||||
- Hash Rate by Engine (trend):
|
||||
- `sum by (engine) (max_over_time(miner_job_hash_rate{engine=~"$engine"}[1m]))`
|
||||
- Jobs by Engine and Status:
|
||||
- `sum by (engine, status) (increase(miner_jobs_by_engine_total{engine=~"$engine"}[1m]))`
|
||||
- Per-Engine Total Hashes:
|
||||
- `sum by (engine) (increase(miner_job_hashes_total{engine=~"$engine"}[1m]))`
|
||||
- Per-Job / Per-Thread (trend):
|
||||
- `max_over_time(miner_job_hash_rate{...}[1m])`
|
||||
- `max_over_time(miner_thread_hash_rate{...}[1m])`
|
||||
|
||||
### 3) Instances Comparison — `instances-comparison-dashboard.json`
|
||||
|
||||
- Added a `job` filter variable (multi‑select) alongside `instance` and `engine`, and applied `job=~"$job"` across queries to filter by scrape job (e.g., chain).
|
||||
- Global Hash Rate by Instance (trend):
|
||||
- `sum by (instance) (max_over_time(miner_job_hash_rate{job=~"$job", instance=~"$instance"}[1m]))`
|
||||
- Per‑Instance Job Hash Rate (trend):
|
||||
- `sum by (instance) (max_over_time(miner_job_hash_rate{job=~"$job", instance=~"$instance", engine=~"$engine"}[1m]))`
|
||||
- Current vs 1h Peak (per instance) — converted to a table with delta:
|
||||
- Current (1m max):
|
||||
- `sum by (instance) (max_over_time(miner_job_hash_rate{job=~"$job", instance=~"$instance", engine=~"$engine"}[1m]))`
|
||||
- Peak 1h (1m resolution subquery):
|
||||
- `sum by (instance) (max_over_time(miner_job_hash_rate{job=~"$job", instance=~"$instance", engine=~"$engine"}[1h:1m]))`
|
||||
- Delta:
|
||||
- Calculated as `current / peak_1h` via a table transformation (`calculateField`).
|
||||
- Columns: `instance | current_ops | peak_1h_ops | delta_current_over_peak`
|
||||
- 24h Peak Hash Rate (selected instances):
|
||||
- `sum by (instance) (max_over_time(miner_job_hash_rate{job=~"$job", instance=~"$instance"}[24h:1m]))`
|
||||
- New panels to disambiguate idle vs active:
|
||||
- Active Jobs (by instance):
|
||||
- `sum by (instance) (miner_active_jobs{job=~"$job", instance=~"$instance"})`
|
||||
- Mine Requests (5m increase; by instance & result):
|
||||
- `sum by (instance, result) (increase(miner_mine_requests_total{job=~"$job", instance=~"$instance"}[5m]))`
|
||||
|
||||
---
|
||||
|
||||
## Prometheus Settings
|
||||
|
||||
- Keep global defaults (e.g., `scrape_interval: 15s`), but override miner jobs to 2s:
|
||||
- `scrape_interval: 2s`, `scrape_timeout: 1.5s`
|
||||
- For short-lived performance experiments, consider 1s; for load-sensitive prod, 5s works with chunking still providing reasonable freshness.
|
||||
- Align `--progress-chunk-ms` with your scrape interval (e.g., chunk ~1000–2000ms for a 2s scrape).
|
||||
|
||||
---
|
||||
|
||||
## Operational Results
|
||||
|
||||
- Job/global rates are now responsive while work is active and disappear cleanly after completion.
|
||||
- Trend panels (using `max_over_time` / `increase`) provide a meaningful “recent performance” view for engine/instance comparisons.
|
||||
- New service-level metrics (`miner_active_jobs`, `miner_mine_requests_total`) make it obvious when a miner is idle because no jobs are being issued (e.g., on a quiet chain).
|
||||
- Label formatting is consistent and readable across dashboards.
|
||||
|
||||
---
|
||||
|
||||
## Next Actions (Optional Enhancements)
|
||||
|
||||
- Auto‑tune chunk size based on observed per‑thread throughput in the first chunks.
|
||||
- Expose EMA alpha (smoothing) as a knob for advanced tuning.
|
||||
- Add field overrides to render the delta column as a percentage with thresholds.
|
||||
- Provide baseline alerts/recording rules, e.g.:
|
||||
- “No active_jobs for X minutes” (node not issuing /mine).
|
||||
- “Hash rate below expected baseline for Y minutes.”
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference
|
||||
|
||||
- CLI:
|
||||
- `--engine cpu-baseline|cpu-fast`
|
||||
- `--progress-chunk-ms <ms>` (env: `MINER_PROGRESS_CHUNK_MS`)
|
||||
- `--metrics-port <port>`
|
||||
- `--cores <n>` (alias: `--num-cores`)
|
||||
- New metrics:
|
||||
- `miner_active_jobs` (gauge)
|
||||
- `miner_mine_requests_total{result}` (counter)
|
||||
- Rate series handling:
|
||||
- Per-thread and per-job rate series are removed on end, avoiding zero artifacts.
|
||||
- Dashboards:
|
||||
- `miner-dashboard.json` — Operator overview
|
||||
- `engines-comparison-dashboard.json` — Engine comparability
|
||||
- `instances-comparison-dashboard.json` — Per-instance analysis with `job` filter
|
||||
|
||||
These changes yield accurate, timely, and comparative metrics for evaluating engine performance and instance health across your testnets and (eventually) production.
|
||||
Reference in New Issue
Block a user