Skip to content

Commit 22029fd

Browse files
authored
Merge pull request #352 from Shopify/ms.use-analyser-4-limitz
Implement Dynamic Resource Limits Adjustment in Function Runner Using ScaleLimitsAnalyzer
2 parents cead712 + 529a290 commit 22029fd

File tree

9 files changed

+302
-18
lines changed

9 files changed

+302
-18
lines changed

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ rmp-serde = "1.3"
3535
is-terminal = "0.4.13"
3636
wasmprof = "0.7.0"
3737
bluejay-core = { version = "=0.2.0" }
38-
bluejay-parser = { version = "=0.2.0" }
38+
bluejay-parser = { version = "=0.2.0", features = ["format-errors"] }
3939
bluejay-validator = { version = "=0.2.0" }
4040

4141
[dev-dependencies]

src/bluejay_schema_analyzer.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ mod tests {
207207
}
208208

209209
#[test]
210-
fn test_accurate_scale_limits_for_nested_array() {
210+
fn test_no_double_counting_for_duplicate_fields_with_array() {
211211
let schema_string = r#"
212212
directive @scaleLimits(rate: Float!) on FIELD_DEFINITION
213213
type Query {
@@ -241,7 +241,7 @@ mod tests {
241241
}
242242

243243
#[test]
244-
fn test_no_double_counting_for_duplicate_fields_with_nested_array() {
244+
fn test_scale_factor_with_nested_array() {
245245
let schema_string = r#"
246246
directive @scaleLimits(rate: Float!) on FIELD_DEFINITION
247247
type Query {

src/engine.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ pub struct FunctionRunParams<'a> {
5353
pub input: Vec<u8>,
5454
pub export: &'a str,
5555
pub profile_opts: Option<&'a ProfileOpts>,
56+
pub scale_factor: f64,
5657
}
5758

5859
const STARTING_FUEL: u64 = u64::MAX;
@@ -114,6 +115,7 @@ pub fn run(params: FunctionRunParams) -> Result<FunctionRunResult> {
114115
input,
115116
export,
116117
profile_opts,
118+
scale_factor,
117119
} = params;
118120

119121
let engine = Engine::new(
@@ -231,6 +233,7 @@ pub fn run(params: FunctionRunParams) -> Result<FunctionRunResult> {
231233
input: function_run_input,
232234
output,
233235
profile: profile_data,
236+
scale_factor,
234237
};
235238

236239
Ok(function_run_result)

src/function_run_result.rs

Lines changed: 121 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,27 +28,66 @@ pub struct FunctionRunResult {
2828
pub output: FunctionOutput,
2929
#[serde(skip)]
3030
pub profile: Option<String>,
31+
#[serde(skip)]
32+
pub scale_factor: f64,
33+
}
34+
35+
const DEFAULT_INSTRUCTIONS_LIMIT: u64 = 11_000_000;
36+
const DEFAULT_INPUT_SIZE_LIMIT: u64 = 64_000;
37+
const DEFAULT_OUTPUT_SIZE_LIMIT: u64 = 20_000;
38+
39+
pub fn get_json_size_as_bytes(value: &serde_json::Value) -> usize {
40+
serde_json::to_vec(value).map(|v| v.len()).unwrap_or(0)
3141
}
3242

3343
impl FunctionRunResult {
3444
pub fn to_json(&self) -> String {
3545
serde_json::to_string_pretty(&self).unwrap_or_else(|error| error.to_string())
3646
}
47+
48+
pub fn input_size(&self) -> usize {
49+
get_json_size_as_bytes(&self.input)
50+
}
51+
52+
pub fn output_size(&self) -> usize {
53+
match &self.output {
54+
FunctionOutput::JsonOutput(value) => get_json_size_as_bytes(value),
55+
FunctionOutput::InvalidJsonOutput(_value) => 0,
56+
}
57+
}
58+
}
59+
60+
fn humanize_size(title: &str, size_bytes: u64, size_limit: u64) -> String {
61+
let size_humanized = match size_bytes {
62+
0..=1023 => format!("{}B", size_bytes),
63+
1024..=1_048_575 => format!("{:.2}KB", size_bytes as f64 / 1024.0),
64+
1_048_576..=1_073_741_823 => format!("{:.2}MB", size_bytes as f64 / 1_048_576.0),
65+
_ => {
66+
format!("{:.2}GB", size_bytes as f64 / 1_073_741_824.0)
67+
}
68+
};
69+
70+
if size_bytes > size_limit {
71+
format!("{}: {}", title, size_humanized).red().to_string()
72+
} else {
73+
format!("{}: {}", title, size_humanized)
74+
}
3775
}
3876

39-
fn humanize_instructions(instructions: u64) -> String {
77+
fn humanize_instructions(title: &str, instructions: u64, instructions_limit: u64) -> String {
4078
let instructions_humanized = match instructions {
4179
0..=999 => instructions.to_string(),
4280
1000..=999_999 => format!("{}K", instructions as f64 / 1000.0),
4381
1_000_000..=999_999_999 => format!("{}M", instructions as f64 / 1_000_000.0),
4482
1_000_000_000..=u64::MAX => format!("{}B", instructions as f64 / 1_000_000_000.0),
4583
};
4684

47-
match instructions {
48-
0..=11_000_000 => format!("Instructions: {instructions_humanized}"),
49-
11_000_001..=u64::MAX => format!("Instructions: {instructions_humanized}")
85+
if instructions > instructions_limit {
86+
format!("{}: {}", title, instructions_humanized)
5087
.red()
51-
.to_string(),
88+
.to_string()
89+
} else {
90+
format!("{}: {}", title, instructions_humanized)
5291
}
5392
}
5493

@@ -107,15 +146,83 @@ impl fmt::Display for FunctionRunResult {
107146
}
108147
}
109148

149+
let input_size_limit = self.scale_factor * DEFAULT_INPUT_SIZE_LIMIT as f64;
150+
let output_size_limit = self.scale_factor * DEFAULT_OUTPUT_SIZE_LIMIT as f64;
151+
let instructions_size_limit = self.scale_factor * DEFAULT_INSTRUCTIONS_LIMIT as f64;
152+
153+
writeln!(
154+
formatter,
155+
"\n{}\n\n",
156+
" Resource Limits "
157+
.black()
158+
.on_bright_magenta()
159+
)?;
160+
161+
writeln!(
162+
formatter,
163+
"{}",
164+
humanize_size(
165+
"Input Size",
166+
input_size_limit as u64,
167+
input_size_limit as u64
168+
)
169+
)?;
170+
171+
writeln!(
172+
formatter,
173+
"{}",
174+
humanize_size(
175+
"Output Size",
176+
output_size_limit as u64,
177+
output_size_limit as u64
178+
)
179+
)?;
180+
writeln!(
181+
formatter,
182+
"{}",
183+
humanize_instructions(
184+
"Instructions",
185+
instructions_size_limit as u64,
186+
instructions_size_limit as u64
187+
)
188+
)?;
189+
110190
let title = " Benchmark Results "
111191
.black()
112192
.on_truecolor(150, 191, 72);
113193

114194
write!(formatter, "\n\n{title}\n\n")?;
115195
writeln!(formatter, "Name: {}", self.name)?;
116196
writeln!(formatter, "Linear Memory Usage: {}KB", self.memory_usage)?;
117-
writeln!(formatter, "{}", humanize_instructions(self.instructions))?;
118-
writeln!(formatter, "Size: {}KB\n", self.size)?;
197+
writeln!(
198+
formatter,
199+
"{}",
200+
humanize_instructions(
201+
"Instructions",
202+
self.instructions,
203+
instructions_size_limit as u64
204+
)
205+
)?;
206+
writeln!(
207+
formatter,
208+
"{}",
209+
humanize_size(
210+
"Input Size",
211+
self.input_size() as u64,
212+
input_size_limit as u64,
213+
)
214+
)?;
215+
writeln!(
216+
formatter,
217+
"{}",
218+
humanize_size(
219+
"Output Size",
220+
self.output_size() as u64,
221+
output_size_limit as u64,
222+
)
223+
)?;
224+
225+
writeln!(formatter, "Module Size: {}KB\n", self.size)?;
119226

120227
Ok(())
121228
}
@@ -145,11 +252,15 @@ mod tests {
145252
"test": "test"
146253
})),
147254
profile: None,
255+
scale_factor: 1.0,
148256
};
149257

150258
let predicate = predicates::str::contains("Instructions: 1.001K")
151259
.and(predicates::str::contains("Linear Memory Usage: 1000KB"))
152-
.and(predicates::str::contains(expected_input_display));
260+
.and(predicates::str::contains(expected_input_display))
261+
.and(predicates::str::contains("Input Size: 28B"))
262+
.and(predicates::str::contains("Output Size: 15B"));
263+
assert!(predicate.eval(&function_run_result.to_string()));
153264

154265
assert!(predicate.eval(&function_run_result.to_string()));
155266
Ok(())
@@ -172,6 +283,7 @@ mod tests {
172283
"test": "test"
173284
})),
174285
profile: None,
286+
scale_factor: 1.0,
175287
};
176288

177289
let predicate = predicates::str::contains("Instructions: 1")
@@ -198,6 +310,7 @@ mod tests {
198310
"test": "test"
199311
})),
200312
profile: None,
313+
scale_factor: 1.0,
201314
};
202315

203316
let predicate = predicates::str::contains("Instructions: 999")

src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
pub mod bluejay_schema_analyzer;
12
pub mod engine;
23
pub mod function_run_result;
34
pub mod logs;
5+
pub mod scale_limits_analyzer;

src/main.rs

Lines changed: 59 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,15 @@ use std::{
66

77
use anyhow::{anyhow, Result};
88
use clap::{Parser, ValueEnum};
9-
use function_runner::engine::{run, FunctionRunParams, ProfileOpts};
9+
use function_runner::{
10+
bluejay_schema_analyzer::BluejaySchemaAnalyzer,
11+
engine::{run, FunctionRunParams, ProfileOpts},
12+
};
1013

1114
use is_terminal::IsTerminal;
1215

1316
const PROFILE_DEFAULT_INTERVAL: u32 = 500_000; // every 5us
17+
const DEFAULT_SCALE_FACTOR: f64 = 1.0;
1418

1519
/// Supported input flavors
1620
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
@@ -60,6 +64,14 @@ struct Opts {
6064

6165
#[clap(short = 'c', long, value_enum, default_value = "json")]
6266
codec: Codec,
67+
68+
/// Path to graphql file containing Function schema; if omitted, defaults will be used to calculate limits.
69+
#[clap(short = 's', long)]
70+
schema_path: Option<PathBuf>,
71+
72+
/// Path to graphql file containing Function input query; if omitted, defaults will be used to calculate limits.
73+
#[clap(short = 'q', long)]
74+
query_path: Option<PathBuf>,
6375
}
6476

6577
impl Opts {
@@ -89,6 +101,25 @@ impl Opts {
89101

90102
path
91103
}
104+
105+
pub fn read_schema_to_string(&self) -> Option<Result<String>> {
106+
self.schema_path.as_ref().map(read_file_to_string)
107+
}
108+
109+
pub fn read_query_to_string(&self) -> Option<Result<String>> {
110+
self.query_path.as_ref().map(read_file_to_string)
111+
}
112+
}
113+
114+
fn read_file_to_string(file_path: &PathBuf) -> Result<String> {
115+
let mut file = File::open(file_path)
116+
.map_err(|e| anyhow!("Couldn't open file {}: {}", file_path.to_string_lossy(), e))?;
117+
118+
let mut contents = String::new();
119+
file.read_to_string(&mut contents)
120+
.map_err(|e| anyhow!("Couldn't read file {}: {}", file_path.to_string_lossy(), e))?;
121+
122+
Ok(contents)
92123
}
93124

94125
fn main() -> Result<()> {
@@ -109,27 +140,48 @@ fn main() -> Result<()> {
109140
let mut buffer = Vec::new();
110141
input.read_to_end(&mut buffer)?;
111142

112-
let buffer = match opts.codec {
143+
let schema_string = opts.read_schema_to_string().transpose()?;
144+
145+
let query_string = opts.read_query_to_string().transpose()?;
146+
147+
let (json_value, buffer) = match opts.codec {
113148
Codec::Json => {
114-
let _ = serde_json::from_slice::<serde_json::Value>(&buffer)
149+
let json = serde_json::from_slice::<serde_json::Value>(&buffer)
115150
.map_err(|e| anyhow!("Invalid input JSON: {}", e))?;
116-
buffer
151+
(Some(json), buffer)
117152
}
118-
Codec::Raw => buffer,
153+
Codec::Raw => (None, buffer),
119154
Codec::JsonToMessagepack => {
120155
let json: serde_json::Value = serde_json::from_slice(&buffer)
121156
.map_err(|e| anyhow!("Invalid input JSON: {}", e))?;
122-
rmp_serde::to_vec(&json)
123-
.map_err(|e| anyhow!("Couldn't convert JSON to MessagePack: {}", e))?
157+
let bytes = rmp_serde::to_vec(&json)
158+
.map_err(|e| anyhow!("Couldn't convert JSON to MessagePack: {}", e))?;
159+
(Some(json), bytes)
124160
}
125161
};
126162

163+
let scale_factor = if let (Some(schema_string), Some(query_string), Some(json_value)) =
164+
(schema_string, query_string, json_value)
165+
{
166+
BluejaySchemaAnalyzer::analyze_schema_definition(
167+
&schema_string,
168+
opts.schema_path.as_ref().and_then(|p| p.to_str()),
169+
&query_string,
170+
opts.query_path.as_ref().and_then(|p| p.to_str()),
171+
&json_value,
172+
)?
173+
} else {
174+
DEFAULT_SCALE_FACTOR // Use default scale factor when schema or query is missing
175+
};
176+
127177
let profile_opts = opts.profile_opts();
178+
128179
let function_run_result = run(FunctionRunParams {
129180
function_path: opts.function,
130181
input: buffer,
131182
export: opts.export.as_ref(),
132183
profile_opts: profile_opts.as_ref(),
184+
scale_factor,
133185
})?;
134186

135187
if opts.json {

tests/fixtures/query/query.graphql

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
query {
2+
cart {
3+
lines {
4+
quantity
5+
}
6+
}
7+
}

tests/fixtures/schema/schema.graphql

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
schema {
2+
query: Query
3+
}
4+
5+
directive @scaleLimits(rate: Float!) on FIELD_DEFINITION
6+
7+
type Attribute {
8+
key: String!
9+
value: String
10+
}
11+
12+
type Cart {
13+
lines: [CartLine!]! @scaleLimits(rate: 0.005)
14+
}
15+
16+
type CartLine {
17+
quantity: Int!
18+
}
19+
20+
type Query {
21+
cart: Cart
22+
}

0 commit comments

Comments
 (0)