Skip to content

Commit dd48f0a

Browse files
committed
Optimize jni
1 parent 1d3b570 commit dd48f0a

47 files changed

Lines changed: 1161 additions & 515 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

benches/compile-bench-runner/src/main.rs

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -58,12 +58,17 @@ fn parse_args() -> Args {
5858
};
5959
let mut it = env::args().skip(1);
6060
while let Some(arg) = it.next() {
61-
let mut next = |flag: &str| it.next().unwrap_or_else(|| fatal(&format!("{flag} needs a value")));
61+
let mut next = |flag: &str| {
62+
it.next()
63+
.unwrap_or_else(|| fatal(&format!("{flag} needs a value")))
64+
};
6265
match arg.as_str() {
6366
"--target" => a.target = next("--target"),
6467
"--pass" => a.pass = next("--pass"),
6568
"--runs" => {
66-
a.runs = next("--runs").parse().unwrap_or_else(|_| fatal("--runs must be an integer"));
69+
a.runs = next("--runs")
70+
.parse()
71+
.unwrap_or_else(|_| fatal("--runs must be an integer"));
6772
}
6873
"--save-baseline" => a.save_baseline = Some(next("--save-baseline")),
6974
"--baseline" => a.baseline = Some(next("--baseline")),
@@ -110,7 +115,16 @@ fn measure_once(target: &str, pass: &str) -> Option<f64> {
110115
// Force a full re-expansion of the fixture lib (deps stay built).
111116
let _ = cargo().args(["clean", "-p", target]).status();
112117
let out = cargo()
113-
.args(["rustc", "--quiet", "-p", target, "--lib", "--", "-Z", "time-passes"])
118+
.args([
119+
"rustc",
120+
"--quiet",
121+
"-p",
122+
target,
123+
"--lib",
124+
"--",
125+
"-Z",
126+
"time-passes",
127+
])
114128
.output()
115129
.ok()?;
116130
let stderr = String::from_utf8_lossy(&out.stderr);
@@ -166,7 +180,11 @@ fn main() {
166180
eprintln!(" run {:>2}: {t:.4}s", i + 1);
167181
samples.push(t);
168182
}
169-
None => eprintln!(" run {:>2}: pass `{}` not found in output", i + 1, args.pass),
183+
None => eprintln!(
184+
" run {:>2}: pass `{}` not found in output",
185+
i + 1,
186+
args.pass
187+
),
170188
}
171189
}
172190
if samples.is_empty() {
@@ -175,8 +193,16 @@ fn main() {
175193

176194
samples.sort_by(|a, b| a.partial_cmp(b).unwrap());
177195
let med0 = median(&samples);
178-
let clean: Vec<f64> = samples.iter().copied().filter(|&t| t <= med0 * 3.0).collect();
179-
let clean = if clean.is_empty() { samples.clone() } else { clean };
196+
let clean: Vec<f64> = samples
197+
.iter()
198+
.copied()
199+
.filter(|&t| t <= med0 * 3.0)
200+
.collect();
201+
let clean = if clean.is_empty() {
202+
samples.clone()
203+
} else {
204+
clean
205+
};
180206

181207
let min = clean[0];
182208
let med = median(&clean);
@@ -209,8 +235,7 @@ fn main() {
209235
let path = baselines_dir().join(format!("{name}.txt"));
210236
match fs::read_to_string(&path) {
211237
Ok(s) => {
212-
let mut base: Vec<f64> =
213-
s.lines().filter_map(|l| l.trim().parse().ok()).collect();
238+
let mut base: Vec<f64> = s.lines().filter_map(|l| l.trim().parse().ok()).collect();
214239
if base.is_empty() {
215240
eprintln!(" baseline `{name}` is empty");
216241
} else {

benches/macro-compile-bench/src/models/schemas.rs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -171,12 +171,21 @@ pub struct ErrorBody {
171171
impl<T: Serialize> Paginated<T> {
172172
/// One empty page — keeps handlers free of `T` construction.
173173
pub fn empty() -> Self {
174-
Self { items: Vec::new(), total: 0, page: 1, per_page: 20 }
174+
Self {
175+
items: Vec::new(),
176+
total: 0,
177+
page: 1,
178+
per_page: 20,
179+
}
175180
}
176181
}
177182

178183
impl<T: Serialize> ApiResponse<T> {
179184
pub fn ok(data: T) -> Self {
180-
Self { data, success: true, message: None }
185+
Self {
186+
data,
187+
success: true,
188+
message: None,
189+
}
181190
}
182191
}

crates/vespera/src/multipart.rs

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -159,11 +159,11 @@ impl TypedMultipartError {
159159
/// it describes a client-supplied field problem). The full `Display`
160160
/// (including `Other`'s `source`) stays available for server-side
161161
/// logging via the `std::error::Error` impl.
162-
fn response_message(&self) -> String {
162+
fn response_message(&self) -> Cow<'_, str> {
163163
if matches!(self, Self::Other { .. }) {
164-
"internal error while processing multipart request".to_owned()
164+
Cow::Borrowed("internal error while processing multipart request")
165165
} else {
166-
self.to_string()
166+
Cow::Owned(self.to_string())
167167
}
168168
}
169169
}
@@ -221,7 +221,9 @@ impl IntoResponse for TypedMultipartError {
221221
// fallback keeps this request-time error path panic-free (matching
222222
// `Validated<T>`'s 422 envelope) by emitting a minimal valid envelope
223223
// instead of unwinding inside a handler.
224-
.unwrap_or_else(|_| br#"{"errors":[{"message":"serialization error","path":""}]}"#.to_vec());
224+
.unwrap_or_else(|_| {
225+
br#"{"errors":[{"message":"serialization error","path":""}]}"#.to_vec()
226+
});
225227
(
226228
status,
227229
[(
@@ -492,6 +494,12 @@ fn str_to_bool(s: &str) -> Option<bool> {
492494
/// an explicit `#[form_data(limit = "...")]`.
493495
const DEFAULT_STRING_FIELD_LIMIT_BYTES: usize = 1024 * 1024; // 1 MiB
494496

497+
/// Default streaming cap for an **unannotated** `NamedTempFile` multipart field.
498+
///
499+
/// Explicit `#[form_data(limit = "unlimited")]` continues to opt out by passing
500+
/// `usize::MAX` through the derive-generated parser.
501+
const DEFAULT_TEMP_FILE_FIELD_LIMIT_BYTES: usize = 1024 * 1024; // 1 MiB
502+
495503
impl<S: Send + Sync> TryFromFieldWithState<S> for String {
496504
async fn try_from_field_with_state(
497505
field: Field<'_>,
@@ -626,18 +634,17 @@ impl<S: Send + Sync> TryFromFieldWithState<S> for tempfile::NamedTempFile {
626634
})?;
627635
let mut file = tokio::fs::File::from_std(std_file);
628636

637+
let limit_bytes = limit_bytes.unwrap_or(DEFAULT_TEMP_FILE_FIELD_LIMIT_BYTES);
629638
let mut total = 0usize;
630639
while let Some(chunk) = field.chunk().await? {
631640
// `saturating_add` (matching `read_field_data`) prevents a
632641
// pathological chunk size from wrapping `total` and slipping
633642
// past the limit check below.
634643
total = total.saturating_add(chunk.len());
635-
if let Some(limit) = limit_bytes
636-
&& total > limit
637-
{
644+
if total > limit_bytes {
638645
return Err(TypedMultipartError::FieldTooLarge {
639646
field_name: field.name().unwrap_or_default().to_string(),
640-
limit_bytes: limit,
647+
limit_bytes,
641648
});
642649
}
643650
tokio::io::AsyncWriteExt::write_all(&mut file, &chunk)

crates/vespera/src/validated.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,16 @@ where
9494
}
9595
}
9696

97+
impl<U> ValidatePayload for crate::multipart::TypedMultipart<U>
98+
where
99+
U: Validate<Context = ()>,
100+
{
101+
type Inner = U;
102+
fn payload(&self) -> &U {
103+
&self.0
104+
}
105+
}
106+
97107
impl<S, T> FromRequest<S> for Validated<T>
98108
where
99109
S: Send + Sync,

crates/vespera/tests/multipart_wire.rs

Lines changed: 101 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ use ::tokio::runtime::Builder;
2020
use ::vespera::axum::Json;
2121
use ::vespera::multipart::{FieldData, TypedMultipart};
2222
use ::vespera::tempfile::NamedTempFile;
23-
use ::vespera::{Multipart, Schema};
23+
use ::vespera::{Multipart, Schema, Validated};
2424
use ::vespera_inprocess::{dispatch_from_bytes, register_app};
2525

2626
#[derive(Multipart, Schema)]
@@ -34,6 +34,20 @@ struct UploadReq {
3434
file: FieldData<NamedTempFile>,
3535
}
3636

37+
#[derive(Multipart, Schema)]
38+
#[allow(dead_code)]
39+
struct CappedUploadReq {
40+
name: String,
41+
file: FieldData<NamedTempFile>,
42+
}
43+
44+
#[derive(Multipart, Schema, garde::Validate)]
45+
#[allow(dead_code)]
46+
struct ValidatedMultipartReq {
47+
#[garde(length(min = 3))]
48+
name: String,
49+
}
50+
3751
#[derive(Serialize, Schema)]
3852
struct UploadResult {
3953
name: String,
@@ -59,6 +73,29 @@ async fn upload_handler(TypedMultipart(mut req): TypedMultipart<UploadReq>) -> J
5973
})
6074
}
6175

76+
async fn capped_upload_handler(
77+
TypedMultipart(mut req): TypedMultipart<CappedUploadReq>,
78+
) -> Json<UploadResult> {
79+
let mut buf = Vec::new();
80+
let f = req.file.contents.as_file_mut();
81+
f.seek(SeekFrom::Start(0)).expect("rewind temp file");
82+
f.read_to_end(&mut buf).expect("read temp file");
83+
Json(UploadResult {
84+
name: req.name,
85+
file_size: u64::try_from(buf.len()).expect("file size fits in u64"),
86+
file_first_byte: *buf.first().unwrap_or(&0),
87+
file_last_byte: *buf.last().unwrap_or(&0),
88+
})
89+
}
90+
91+
async fn validated_multipart_handler(
92+
Validated(TypedMultipart(req)): Validated<TypedMultipart<ValidatedMultipartReq>>,
93+
) -> Json<TextResult> {
94+
Json(TextResult {
95+
text_len: u64::try_from(req.name.len()).unwrap_or(u64::MAX),
96+
})
97+
}
98+
6299
/// Unannotated `String` field — inherits the default 1 MiB cap.
63100
#[derive(Multipart, Schema)]
64101
#[allow(dead_code)]
@@ -96,6 +133,8 @@ async fn text_unlimited_handler(
96133
fn multipart_router() -> Router {
97134
Router::new()
98135
.route("/upload", post(upload_handler))
136+
.route("/capped-upload", post(capped_upload_handler))
137+
.route("/validated-multipart", post(validated_multipart_handler))
99138
.route("/text", post(text_handler))
100139
.route("/text-unlimited", post(text_unlimited_handler))
101140
// Disable the 2 MiB default so the 256 KiB test below isn't
@@ -116,6 +155,16 @@ fn encode_multipart_wire(
116155
name: &str,
117156
file_name: &str,
118157
file_bytes: &[u8],
158+
) -> Vec<u8> {
159+
encode_multipart_upload_wire("/upload", boundary, name, file_name, file_bytes)
160+
}
161+
162+
fn encode_multipart_upload_wire(
163+
path: &str,
164+
boundary: &str,
165+
name: &str,
166+
file_name: &str,
167+
file_bytes: &[u8],
119168
) -> Vec<u8> {
120169
let mut body = Vec::new();
121170
body.extend_from_slice(format!("--{boundary}\r\n").as_bytes());
@@ -139,7 +188,7 @@ fn encode_multipart_wire(
139188
let header_json = ::serde_json::json!({
140189
"v": 1,
141190
"method": "POST",
142-
"path": "/upload",
191+
"path": path,
143192
"headers": headers,
144193
});
145194
let header_bytes = ::serde_json::to_vec(&header_json).expect("header serialise");
@@ -341,3 +390,53 @@ fn string_field_unlimited_optout_allows_large() {
341390
let json: Value = ::serde_json::from_slice(&body).expect("response is JSON");
342391
assert_eq!(json["text_len"].as_u64(), Some(1024 * 1024 + 1));
343392
}
393+
394+
#[test]
395+
fn named_temp_file_over_default_cap_rejected_413() {
396+
install_router_once();
397+
let runtime = Builder::new_current_thread()
398+
.enable_all()
399+
.build()
400+
.expect("tokio runtime");
401+
402+
let payload = vec![b'z'; 1024 * 1024 + 1];
403+
let wire = encode_multipart_upload_wire(
404+
"/capped-upload",
405+
"----TempFileCapBoundary",
406+
"bob",
407+
"too-large.bin",
408+
&payload,
409+
);
410+
let resp = dispatch_from_bytes(wire, &runtime);
411+
let (header, _body) = decode_wire(&resp);
412+
assert_eq!(
413+
header["status"].as_u64(),
414+
Some(413),
415+
"oversized unannotated tempfile field must be rejected with 413, got header={header:#}"
416+
);
417+
}
418+
419+
#[test]
420+
fn validated_typed_multipart_rejects_garde_failure_422() {
421+
install_router_once();
422+
let runtime = Builder::new_current_thread()
423+
.enable_all()
424+
.build()
425+
.expect("tokio runtime");
426+
427+
let wire = encode_multipart_text(
428+
"----ValidatedMultipartBoundary",
429+
"/validated-multipart",
430+
"name",
431+
b"xy",
432+
);
433+
let resp = dispatch_from_bytes(wire, &runtime);
434+
let (header, body) = decode_wire(&resp);
435+
assert_eq!(
436+
header["status"].as_u64(),
437+
Some(422),
438+
"garde failure must be rejected with 422, got header={header:#}"
439+
);
440+
let json: Value = ::serde_json::from_slice(&body).expect("response is JSON");
441+
assert_eq!(json["errors"][0]["path"], "name");
442+
}

crates/vespera_core/src/openapi.rs

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -286,16 +286,15 @@ impl OpenApi {
286286
self.external_docs = other.external_docs;
287287
}
288288

289-
// Merge tags, de-duplicating by name in a single pass with first-wins
290-
// semantics (existing tags and already-appended incoming tags both
291-
// win; incoming insertion order preserved). Tag lists are tiny, so a
292-
// linear membership scan over `self_tags` beats a `HashSet` here: it
293-
// allocates nothing and clones nothing — the kept tag is *moved* in,
294-
// and a duplicate is detected by borrow and skipped.
289+
// Merge tags, de-duplicating by name with first-wins semantics while
290+
// preserving deterministic output order (existing tags first, then
291+
// incoming tags in their original order).
295292
if let Some(other_tags) = other.tags {
296293
let self_tags = self.tags.get_or_insert_with(Vec::new);
294+
let mut seen: std::collections::HashSet<String> =
295+
self_tags.iter().map(|tag| tag.name.clone()).collect();
297296
for tag in other_tags {
298-
if !self_tags.iter().any(|existing| existing.name == tag.name) {
297+
if seen.insert(tag.name.clone()) {
299298
self_tags.push(tag);
300299
}
301300
}

crates/vespera_core/src/route.rs

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -238,19 +238,30 @@ pub struct PathItem {
238238
}
239239

240240
impl PathItem {
241-
/// Set an operation for a specific HTTP method
242-
pub fn set_operation(&mut self, method: HttpMethod, operation: Operation) {
241+
/// Try to set an operation for a specific HTTP method.
242+
///
243+
/// Returns the operation that was already present, if this call replaced one.
244+
pub fn try_set_operation(
245+
&mut self,
246+
method: HttpMethod,
247+
operation: Operation,
248+
) -> Option<Operation> {
243249
match method {
244-
HttpMethod::Get => self.get = Some(operation),
245-
HttpMethod::Post => self.post = Some(operation),
246-
HttpMethod::Put => self.put = Some(operation),
247-
HttpMethod::Patch => self.patch = Some(operation),
248-
HttpMethod::Delete => self.delete = Some(operation),
249-
HttpMethod::Head => self.head = Some(operation),
250-
HttpMethod::Options => self.options = Some(operation),
251-
HttpMethod::Trace => self.trace = Some(operation),
250+
HttpMethod::Get => self.get.replace(operation),
251+
HttpMethod::Post => self.post.replace(operation),
252+
HttpMethod::Put => self.put.replace(operation),
253+
HttpMethod::Patch => self.patch.replace(operation),
254+
HttpMethod::Delete => self.delete.replace(operation),
255+
HttpMethod::Head => self.head.replace(operation),
256+
HttpMethod::Options => self.options.replace(operation),
257+
HttpMethod::Trace => self.trace.replace(operation),
252258
}
253259
}
260+
261+
/// Set an operation for a specific HTTP method, discarding any replaced operation.
262+
pub fn set_operation(&mut self, method: HttpMethod, operation: Operation) {
263+
let _ = self.try_set_operation(method, operation);
264+
}
254265
}
255266

256267
#[cfg(test)]

0 commit comments

Comments
 (0)