-
Notifications
You must be signed in to change notification settings - Fork 11.3k
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
rpc benchmark init: directly read DB based on schema #20378
base: main
Are you sure you want to change the base?
Conversation
The latest updates on your projects. Learn more about Vercel for Git ↗︎
3 Skipped Deployments
|
9c98761
to
1069b2c
Compare
1069b2c
to
8b0818c
Compare
8b0818c
to
eb96893
Compare
What is the rational behind these generated queries? Would they be representative to what we are interested in? |
@lxfind the generated queries now are basically "all supported queries based on primary key and indices" and does not consider the representativeness, I plan make it configurable for example taking in a file of weights to make it representative as a followup. |
902755b
to
43ab64b
Compare
43ab64b
to
47dcc16
Compare
489ed78
to
b2c4fac
Compare
/// Duration to run the benchmark in seconds | ||
pub duration: Duration, |
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.
is this per query, or for the entire benchmark? Commenting before looking through rest of code so might become clearer, but still good to revisit and clarify
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.
this is essentially the time-out of the while benchmark run, changed the field name to be more explicit.
#[derive(Clone, Default)] | ||
pub struct MetricsCollector { | ||
metrics: Arc<DashMap<String, QueryMetrics>>, | ||
} | ||
|
||
impl MetricsCollector { |
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.
if we envision MetricsCollector
as an orchestrator of various benchmark reports, I wonder if we can do something similar to indexer alt framework, where record_query
simply passes on received data to each BenchmarkResult
struct, and generate_report
just finish
es each struct
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.
MetricsCollector accumulates the benchmark records in the metrics
map and generates one report out of the map, so not exactly orchestration -- what's the suggested alternative way and what's the advantage ?
let tables_query = r#" | ||
SELECT tablename | ||
FROM pg_tables | ||
WHERE schemaname = 'public' | ||
AND tablename != '__diesel_schema_migrations' | ||
ORDER BY tablename; | ||
"#; | ||
let tables: Vec<String> = client | ||
.query(tables_query, &[]) | ||
.await? | ||
.iter() | ||
.map(|row| row.get::<_, String>(0)) | ||
.collect(); | ||
info!( | ||
"Found {} active tables in database: {:?}", | ||
tables.len(), | ||
tables | ||
); | ||
|
||
let pk_query = r#" | ||
SELECT tc.table_name, kcu.column_name | ||
FROM information_schema.table_constraints tc | ||
JOIN information_schema.key_column_usage kcu | ||
ON tc.constraint_name = kcu.constraint_name | ||
WHERE tc.constraint_type = 'PRIMARY KEY' | ||
AND tc.table_schema = 'public' | ||
AND tc.table_name != '__diesel_schema_migrations' | ||
ORDER BY tc.table_name, kcu.ordinal_position; | ||
"#; |
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.
maybe these queries can be standalone
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.
pk is essentially index + uniqueness, the queries run the same way today, any reason to split them?
impl QueryGenerator { | ||
async fn get_tables_and_indexes(&self) -> Result<Vec<BenchmarkQuery>, anyhow::Error> { |
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.
not sure if this needs to be a struct
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.
what's the advantage of avoiding that?
/// against the database. It can “enrich” each BenchmarkQuery by sampling real | ||
/// data from the relevant table. Each query’s execution is timed and recorded |
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.
/// against the database. It can “enrich” each BenchmarkQuery by sampling real | |
/// data from the relevant table. Each query’s execution is timed and recorded | |
/// against the database. It can “enrich” each BenchmarkQueryTemplate by sampling real | |
/// data from the relevant table. Each query’s execution is timed and recorded |
The generated query templates are on pk and indexes, and need to be enriched with values sampled from the relevant table
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.
it'd be great if we could create general sampling guidelines, i.e select some value that is most represented, rarest, etc.
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.
re being presentative, makes sense and sounds further out, I would like to have a runnable tool and see how it can help us before polishing things.
crates/sui-rpc-benchmark/src/lib.rs
Outdated
let query_generator = QueryGenerator { | ||
db_url: db_url.clone(), | ||
}; | ||
let benchmark_queries = query_generator.generate_benchmark_queries().await?; | ||
info!("Generated {} benchmark queries", benchmark_queries.len()); | ||
|
||
let config = BenchmarkConfig { | ||
concurrency, | ||
duration: Duration::from_secs(duration_secs), | ||
}; | ||
|
||
let mut query_executor = QueryExecutor::new(&db_url, benchmark_queries, config).await?; | ||
let result = query_executor.run().await?; | ||
info!("Total queries: {}", result.total_queries); |
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.
right, i was thinking instead we'd have a
- generate template phase
- enrich template with values sampled from db. or, inject values provided by user. user can set sampling technique
- actual benchmarking
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.
Thanks for adding the doc comments, they really help make sense of the design. Some high level thoughts:
- It would be nice to have a clearer phase separation between doing the work to sample the data from the database, enriching queries, and then running the benchmarks. (I think the first two steps in particular could be separated more).
- It would be very worthwhile to re-use prometheus for implementing various kinds of metric, maybe even exposing them over a metrics service (we have pulled out the metrics service we use in the indexer and RPC into its own crate now as well). This would allow us to focus on worrying about what data we want to track, and not on how to implement, e.g. a sampling histogram.
table_name, | ||
queries: metrics.total_queries, | ||
errors: metrics.errors, | ||
avg_latency_ms: avg_latency, |
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.
It would be good to track percentiles as well as the mean (p50, p90, p99). Can we re-use prometheus for this?
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.
conceptually it's good to also track percentiles and prometheus can come in handy, practically my intent here is to get a sense when DB is overwhelmed, and avg latency suffices for that purpose -- happy to extend that to percentiles if we find outputs of the tool useful and decide to add more granular data.
let params: Vec<Box<dyn ToSql + Sync + Send>> = row | ||
.iter() | ||
.map(|val| match val { | ||
SqlValue::Text(v) => Box::new(v) as Box<dyn ToSql + Sync + Send>, | ||
SqlValue::Int4(v) => Box::new(v) as Box<dyn ToSql + Sync + Send>, | ||
SqlValue::Int8(v) => Box::new(v) as Box<dyn ToSql + Sync + Send>, | ||
SqlValue::Float8(v) => Box::new(v) as Box<dyn ToSql + Sync + Send>, | ||
SqlValue::Bool(v) => Box::new(v) as Box<dyn ToSql + Sync + Send>, | ||
SqlValue::Int2(v) => Box::new(v) as Box<dyn ToSql + Sync + Send>, | ||
SqlValue::Bytea(v) => Box::new(v) as Box<dyn ToSql + Sync + Send>, | ||
}) | ||
.collect(); | ||
let param_refs: Vec<&(dyn ToSql + Sync)> = params | ||
.iter() | ||
.map(|p| p.as_ref() as &(dyn ToSql + Sync)) | ||
.collect(); | ||
|
||
let query_str = enriched.query.query_template.clone(); | ||
|
||
let start = Instant::now(); | ||
let result = client.query(&query_str, ¶m_refs[..]).await; |
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.
The boxing step here should be unnecessary:
let params: Vec<Box<dyn ToSql + Sync + Send>> = row | |
.iter() | |
.map(|val| match val { | |
SqlValue::Text(v) => Box::new(v) as Box<dyn ToSql + Sync + Send>, | |
SqlValue::Int4(v) => Box::new(v) as Box<dyn ToSql + Sync + Send>, | |
SqlValue::Int8(v) => Box::new(v) as Box<dyn ToSql + Sync + Send>, | |
SqlValue::Float8(v) => Box::new(v) as Box<dyn ToSql + Sync + Send>, | |
SqlValue::Bool(v) => Box::new(v) as Box<dyn ToSql + Sync + Send>, | |
SqlValue::Int2(v) => Box::new(v) as Box<dyn ToSql + Sync + Send>, | |
SqlValue::Bytea(v) => Box::new(v) as Box<dyn ToSql + Sync + Send>, | |
}) | |
.collect(); | |
let param_refs: Vec<&(dyn ToSql + Sync)> = params | |
.iter() | |
.map(|p| p.as_ref() as &(dyn ToSql + Sync)) | |
.collect(); | |
let query_str = enriched.query.query_template.clone(); | |
let start = Instant::now(); | |
let result = client.query(&query_str, ¶m_refs[..]).await; | |
let params: Vec<&dyn (ToSql + Sync)>> = row | |
.iter() | |
.map(|val| match val { | |
SqlValue::Text(v) => v, | |
SqlValue::Int4(v) => v, | |
SqlValue::Int8(v) => v, | |
SqlValue::Float8(v) => v, | |
SqlValue::Bool(v) => v, | |
SqlValue::Int2(v) => v, | |
SqlValue::Bytea(v) => v, | |
}) | |
.collect(); | |
let query_str = enriched.query.query_template.clone(); | |
let start = Instant::now(); | |
let result = client.query(&query_str, ¶ms[..]).await; |
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.
it errors out if using & instead of boxing it
cargo build
Compiling sui-rpc-benchmark v1.42.0 (/Users/gegao/Documents/sui/crates/sui-rpc-benchmark)
error[E0308]: `match` arms have incompatible types
--> crates/sui-rpc-benchmark/src/direct/query_executor.rs:160:42
|
158 | .map(|val| match val {
| ____________________________-
159 | | SqlValue::Text(v) => v,
| | - this is found to be of type `&Option<std::string::String>`
160 | | SqlValue::Int4(v) => v,
| | ^ expected `&Option<String>`, found `&Option<i32>`
161 | | SqlValue::Int8(v) => v,
... |
165 | | SqlValue::Bytea(v) => v,
166 | | })
| |_________________- `match` arms have incompatible types
|
= note: expected reference `&Option<std::string::String>`
found reference `&Option<i32>`
For more information about this error, try `rustc --explain E0308`.
error: could not compile `sui-rpc-benchmark` (lib) due to 1 previous error
after tweaking the suggested change to let params: Vec<&dyn ToSql + Sync> = row
, just removing the un-matched >
and un-necessary ()
if self.enriched_queries.is_empty() { | ||
self.initialize_samples().await?; | ||
} |
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.
Do we need to do this in a mutable way like this, or could we have a function that returns a set of enriched queries to run?
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.
sure but it does not matter as initialize_samples
is a single threaded prep step?
b2c4fac
to
5c0e3be
Compare
5c0e3be
to
d6648cc
Compare
d6648cc
to
592ae3d
Compare
Description
main things in this pr
__diesel_schema_migrations
Test plan
sui-indexer-alt-schema
report with local DB
Release notes
Check each box that your changes affect. If none of the boxes relate to your changes, release notes aren't required.
For each box you select, include information after the relevant heading that describes the impact of your changes that a user might notice and any actions they must take to implement updates.