Skip to content

Commit 8641a4b

Browse files
committed
Add benchmarks for search queries and update README with benchmark instructions and results
1 parent d72f101 commit 8641a4b

File tree

3 files changed

+197
-0
lines changed

3 files changed

+197
-0
lines changed

Cargo.toml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,12 @@ serde_json = "1.0"
2727
tokio = { version = "1", features = ["full"] }
2828
# HTTP client
2929
reqwest = "0.11.7"
30+
31+
[dev-dependencies]
32+
criterion = { version = "0.5", features = ["async_tokio", "html_reports"] }
33+
memory-stats = "1.0"
34+
futures = "0.3"
35+
36+
[[bench]]
37+
name = "search_query"
38+
harness = false

README.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,51 @@ It returns your account information.
154154
- Asyncronous HTTP request handle method using tokio and reqwest
155155
- Async tests using Tokio
156156

157+
## Benchmarks
158+
159+
Performance benchmarks are available to measure runtime, memory usage, and CPU performance of search queries.
160+
161+
### Running Benchmarks
162+
163+
```bash
164+
export SERPAPI_KEY=your_api_key_here
165+
cargo bench --bench search_query
166+
```
167+
168+
### Benchmark Results
169+
170+
The benchmark executes the same search query ("coffee" in Austin, TX) 10 times and measures runtime, memory usage, and JSON parsing performance.
171+
172+
#### Search Query Performance
173+
174+
| Metric | Time (mean) | Time (std dev) | Iterations |
175+
|--------|-------------|----------------|------------|
176+
| search_with_parsing | 263.01 ms | 5.61 ms | 10 |
177+
178+
*Detailed results available in `target/criterion/search_query/search_with_parsing/`*
179+
180+
**Latest benchmark results:**
181+
182+
**Runtime Performance:**
183+
- **Mean time:** 263.01 ms
184+
- **Standard deviation:** 5.61 ms
185+
- **95% Confidence interval:** 257.92 ms - 269.14 ms
186+
- **Samples collected:** 10
187+
188+
**Memory Usage:**
189+
- **Memory Delta (mean):** 16.98 KB (0.02 MB)
190+
- **Memory Delta (std dev):** 264.29 KB (0.26 MB)
191+
- **Peak Memory:** 16.05 MB
192+
- **Memory samples:** 455
193+
194+
> **Note:** Benchmark results are generated using [Criterion.rs](https://github.com/bheisler/criterion.rs).
195+
> Open `target/criterion/search_query/search_with_parsing/report/index.html` in your browser to view detailed HTML reports with statistical analysis, plots, and comparisons.
196+
>
197+
> The benchmark measures:
198+
> - Runtime performance of the search API call
199+
> - Memory usage during search operations
200+
> - CPU performance of JSON parsing and data access
201+
157202
### References
158203
* https://www.lpalmieri.com/posts/how-to-write-a-rest-client-in-rust-with-reqwest-and-wiremock/
159204
* Serdes JSON

benches/search_query.rs

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
//! Benchmark suite for SerpAPI search queries
2+
//!
3+
//! This benchmark executes the same search query 10 times and measures:
4+
//! - Runtime performance
5+
//! - Memory usage
6+
//! - CPU performance (JSON parsing)
7+
//!
8+
//! Usage:
9+
//! export API_KEY=your_api_key
10+
//! cargo bench --bench search_query
11+
12+
use criterion::{black_box, criterion_group, criterion_main, Criterion};
13+
use serpapi::serpapi::Client;
14+
use std::collections::HashMap;
15+
use std::sync::{Arc, Mutex};
16+
use tokio::runtime::Runtime;
17+
18+
fn api_key() -> String {
19+
std::env::var("SERPAPI_KEY").unwrap_or_else(|_| {
20+
eprintln!("Warning: SERPAPI_KEY not set. Benchmarks may fail.");
21+
"".to_string()
22+
})
23+
}
24+
25+
fn setup_client() -> Client {
26+
let mut default = HashMap::<String, String>::new();
27+
default.insert("engine".to_string(), "google".to_string());
28+
default.insert("api_key".to_string(), api_key());
29+
Client::new(default)
30+
}
31+
32+
fn setup_search_params() -> HashMap<String, String> {
33+
let mut parameter = HashMap::<String, String>::new();
34+
parameter.insert("q".to_string(), "coffee".to_string());
35+
parameter.insert(
36+
"location".to_string(),
37+
"Austin, TX, Texas, United States".to_string(),
38+
);
39+
parameter
40+
}
41+
42+
fn benchmark_search_query(c: &mut Criterion) {
43+
let rt = Runtime::new().unwrap();
44+
let client = Arc::new(setup_client());
45+
let params = setup_search_params();
46+
47+
// Collect memory statistics separately using Arc<Mutex<>>
48+
let memory_deltas: Arc<Mutex<Vec<usize>>> = Arc::new(Mutex::new(Vec::new()));
49+
let peak_memory: Arc<Mutex<usize>> = Arc::new(Mutex::new(0));
50+
51+
let mut group = c.benchmark_group("search_query");
52+
group.sample_size(10); // Execute 10 times
53+
group.measurement_time(std::time::Duration::from_secs(120)); // Allow up to 120s for API calls
54+
55+
let client_clone = Arc::clone(&client);
56+
let params_clone = params.clone();
57+
let memory_deltas_clone = Arc::clone(&memory_deltas);
58+
let peak_memory_clone = Arc::clone(&peak_memory);
59+
60+
group.bench_function("search_with_parsing", |b| {
61+
let client = Arc::clone(&client_clone);
62+
let params = params_clone.clone();
63+
let memory_deltas = Arc::clone(&memory_deltas_clone);
64+
let peak_memory = Arc::clone(&peak_memory_clone);
65+
66+
b.to_async(&rt).iter(|| {
67+
let client = Arc::clone(&client);
68+
let params = params.clone();
69+
let memory_deltas = Arc::clone(&memory_deltas);
70+
let peak_memory = Arc::clone(&peak_memory);
71+
72+
async move {
73+
// Track memory before
74+
let mem_before = memory_stats::memory_stats()
75+
.map(|m| m.physical_mem)
76+
.unwrap_or(0);
77+
78+
// Perform search using the shared client
79+
let result = client.search(black_box(params)).await;
80+
81+
// CPU-intensive operation: parse and access JSON
82+
if let Ok(json) = result {
83+
let _organic_results = json["organic_results"].as_array();
84+
let _local_results = json["local_results"]["places"].as_array();
85+
let _search_metadata = json["search_metadata"].as_object();
86+
87+
// Track memory after
88+
let mem_after = memory_stats::memory_stats()
89+
.map(|m| m.physical_mem)
90+
.unwrap_or(0);
91+
92+
let mem_delta = mem_after.saturating_sub(mem_before);
93+
94+
// Store memory statistics
95+
if let Ok(mut deltas) = memory_deltas.lock() {
96+
deltas.push(mem_delta);
97+
}
98+
if let Ok(mut peak) = peak_memory.lock() {
99+
*peak = (*peak).max(mem_after);
100+
}
101+
102+
black_box((json, mem_delta))
103+
} else {
104+
black_box((serde_json::Value::Null, 0usize))
105+
}
106+
}
107+
});
108+
});
109+
110+
group.finish();
111+
112+
// Report memory statistics
113+
let deltas = memory_deltas.lock().unwrap();
114+
let peak = peak_memory.lock().unwrap();
115+
116+
if !deltas.is_empty() {
117+
let mean_delta: f64 = deltas.iter().sum::<usize>() as f64 / deltas.len() as f64;
118+
let variance: f64 = deltas.iter()
119+
.map(|&x| {
120+
let diff = x as f64 - mean_delta;
121+
diff * diff
122+
})
123+
.sum::<f64>() / deltas.len() as f64;
124+
let std_dev = variance.sqrt();
125+
126+
println!("\n=== Memory Usage Statistics ===");
127+
println!("Memory Delta (mean): {:.2} KB ({:.2} MB)", mean_delta / 1024.0, mean_delta / (1024.0 * 1024.0));
128+
println!("Memory Delta (std dev): {:.2} KB ({:.2} MB)", std_dev / 1024.0, std_dev / (1024.0 * 1024.0));
129+
println!("Peak Memory: {:.2} KB ({:.2} MB)", *peak as f64 / 1024.0, *peak as f64 / (1024.0 * 1024.0));
130+
println!("Samples: {}", deltas.len());
131+
println!("===============================\n");
132+
}
133+
}
134+
135+
criterion_group! {
136+
name = benches;
137+
config = Criterion::default()
138+
.with_plots()
139+
.with_output_color(true);
140+
targets = benchmark_search_query
141+
}
142+
143+
criterion_main!(benches);

0 commit comments

Comments
 (0)