1use crate::analyzers::{AnalysisContext, Analyzer, Finding};
2use crate::playwright::RenderedPage;
3use crate::storage::{IssueCategory, Severity};
4use crate::CrawlConfig;
5
6pub struct WasmPatternAnalyzer;
16
17impl WasmPatternAnalyzer {
18 #[must_use]
19 pub fn new() -> Self {
20 Self
21 }
22}
23
24impl Default for WasmPatternAnalyzer {
25 fn default() -> Self {
26 Self::new()
27 }
28}
29
30impl Analyzer for WasmPatternAnalyzer {
31 fn name(&self) -> &str {
32 "wasm-pattern"
33 }
34
35 fn analyze(&self, ctx: &AnalysisContext, _config: &CrawlConfig) -> Vec<Finding> {
36 let mut findings = Vec::new();
37 let url = &ctx.page.url;
38 let html: String = ctx
41 .page
42 .scripts
43 .iter()
44 .filter_map(|s| s.src.as_deref())
45 .collect::<Vec<_>>()
46 .join(" ");
47 let html = if html.is_empty() {
48 ctx.page
50 .structured_data
51 .iter()
52 .filter_map(|sd| sd.data.get("url").and_then(|v| v.as_str()))
53 .collect::<Vec<_>>()
54 .join(" ")
55 } else {
56 html
57 };
58
59 if html.contains(".wasm") && !html.contains("rel=\"modulepreload\"") {
61 findings.push(Finding {
62 severity: Severity::Warning,
63 category: IssueCategory::Performance,
64 code: "WASM001".to_string(),
65 title: "Missing WASM module preload".to_string(),
66 description: "Page loads .wasm file without <link rel=\"modulepreload\">. \
67 This delays WASM compilation and hurts Time to Interactive."
68 .to_string(),
69 url: url.to_string(),
70 recommendation: "Add <link rel=\"modulepreload\" href=\"module.wasm\"> for \
71 critical WASM modules."
72 .to_string(),
73 });
74 }
75
76 let sync_patterns = ["WebAssembly.instantiate(", "WebAssembly.compile("];
78 let async_patterns = [
79 "WebAssembly.instantiateStreaming(",
80 "WebAssembly.compileStreaming(",
81 ];
82
83 for pattern in &sync_patterns {
84 if html.contains(pattern) && !async_patterns.iter().any(|a| html.contains(a)) {
85 findings.push(Finding {
86 severity: Severity::Error,
87 category: IssueCategory::Performance,
88 code: "WASM002".to_string(),
89 title: "Synchronous WASM compilation detected".to_string(),
90 description: format!(
91 "Page uses {} which blocks the main thread. \
92 Use streaming compilation instead.",
93 pattern
94 ),
95 url: url.to_string(),
96 recommendation: "Replace WebAssembly.instantiate() with \
97 WebAssembly.instantiateStreaming() for non-blocking compilation."
98 .to_string(),
99 });
100 break;
101 }
102 }
103
104 let has_wasm =
106 html.contains("WebAssembly.instantiate") || html.contains("WebAssembly.compile");
107 let has_try_catch = html.contains("try {") || html.contains("try{");
108 let has_catch = html.contains("catch");
109
110 if has_wasm && !(has_try_catch && has_catch) {
111 findings.push(Finding {
112 severity: Severity::Warning,
113 category: IssueCategory::Custom("Reliability".to_string()),
114 code: "WASM003".to_string(),
115 title: "WASM instantiation without error handling".to_string(),
116 description: "WebAssembly.instantiate/compile called without try/catch. \
117 Unhandled WASM errors will crash the page."
118 .to_string(),
119 url: url.to_string(),
120 recommendation: "Wrap WASM instantiation in try/catch and provide \
121 a JS fallback or user-friendly error message."
122 .to_string(),
123 });
124 }
125
126 findings
127 }
128}
129
130pub struct WasmRuntimeAnalyzer;
138
139impl WasmRuntimeAnalyzer {
140 #[must_use]
141 pub fn new() -> Self {
142 Self
143 }
144}
145
146impl Default for WasmRuntimeAnalyzer {
147 fn default() -> Self {
148 Self::new()
149 }
150}
151
152impl WasmRuntimeAnalyzer {
153 #[must_use]
155 pub fn analyze_rendered(&self, url: &str, rendered: &RenderedPage) -> Vec<Finding> {
156 let mut findings = Vec::new();
157
158 for msg in &rendered.console_messages {
160 if msg.level == "error" && msg.text.to_lowercase().contains("webassembly") {
161 findings.push(Finding {
162 severity: Severity::Error,
163 category: IssueCategory::Custom("Reliability".to_string()),
164 code: "WASM-R001".to_string(),
165 title: "WASM runtime error detected".to_string(),
166 description: format!("Console error: {}", msg.text),
167 url: url.to_string(),
168 recommendation: "Check WASM module integrity and compatibility.".to_string(),
169 });
170 }
171 }
172
173 for msg in &rendered.console_messages {
175 if msg.level == "error"
176 && (msg.text.contains("wasm") || msg.text.contains("WebAssembly"))
177 && (msg.text.contains("load") || msg.text.contains("fetch"))
178 {
179 findings.push(Finding {
180 severity: Severity::Error,
181 category: IssueCategory::Custom("Reliability".to_string()),
182 code: "WASM-R002".to_string(),
183 title: "WASM module load failure".to_string(),
184 description: format!("Console error: {}", msg.text),
185 url: url.to_string(),
186 recommendation: "Verify WASM module URL and CORS configuration.".to_string(),
187 });
188 }
189 }
190
191 for msg in &rendered.console_messages {
193 if msg.level == "warning" && msg.text.to_lowercase().contains("wasm") {
194 findings.push(Finding {
195 severity: Severity::Warning,
196 category: IssueCategory::Performance,
197 code: "WASM-R003".to_string(),
198 title: "WASM deprecation warning".to_string(),
199 description: format!("Console warning: {}", msg.text),
200 url: url.to_string(),
201 recommendation: "Review WASM usage and update if necessary.".to_string(),
202 });
203 }
204 }
205
206 for req in &rendered.network_requests {
208 if req.url.contains(".wasm") && req.status.is_some_and(|s| s >= 400) {
209 findings.push(Finding {
210 severity: Severity::Error,
211 category: IssueCategory::Custom("Reliability".to_string()),
212 code: "WASM-R004".to_string(),
213 title: "WASM module HTTP error".to_string(),
214 description: format!(
215 "WASM module at {} returned HTTP {}",
216 req.url,
217 req.status.unwrap_or(0)
218 ),
219 url: url.to_string(),
220 recommendation: "Verify WASM module availability and CORS headers.".to_string(),
221 });
222 }
223 }
224
225 findings
226 }
227}
228
229pub struct WasmPerformanceAnalyzer;
235
236impl WasmPerformanceAnalyzer {
237 #[must_use]
238 pub fn new() -> Self {
239 Self
240 }
241}
242
243impl Default for WasmPerformanceAnalyzer {
244 fn default() -> Self {
245 Self::new()
246 }
247}
248
249impl WasmPerformanceAnalyzer {
250 #[must_use]
252 pub fn analyze_rendered(&self, url: &str, rendered: &RenderedPage) -> Vec<Finding> {
253 let mut findings = Vec::new();
254
255 let wasm_modules: Vec<_> = rendered
257 .network_requests
258 .iter()
259 .filter(|r| r.url.contains(".wasm"))
260 .collect();
261
262 if wasm_modules.len() > 5 {
263 findings.push(Finding {
264 severity: Severity::Warning,
265 category: IssueCategory::Performance,
266 code: "WASM-P001".to_string(),
267 title: "Too many WASM modules".to_string(),
268 description: format!(
269 "Page loads {} WASM modules. High module count increases memory pressure.",
270 wasm_modules.len()
271 ),
272 url: url.to_string(),
273 recommendation:
274 "Consider consolidating WASM modules or lazy-loading non-critical ones."
275 .to_string(),
276 });
277 }
278
279 let total_wasm_size: u64 = wasm_modules.iter().filter_map(|r| r.size).sum();
281
282 if total_wasm_size > 10 * 1024 * 1024 {
283 findings.push(Finding {
285 severity: Severity::Error,
286 category: IssueCategory::Performance,
287 code: "WASM-P002".to_string(),
288 title: "WASM bundle too large".to_string(),
289 description: format!(
290 "Total WASM size: {:.2} MB. This exceeds the 10 MB recommendation.",
291 total_wasm_size as f64 / (1024.0 * 1024.0)
292 ),
293 url: url.to_string(),
294 recommendation: "Optimize WASM with wasm-opt or split into smaller modules."
295 .to_string(),
296 });
297 }
298
299 if rendered.render_time > std::time::Duration::from_secs(1) {
301 if !wasm_modules.is_empty() {
303 findings.push(Finding {
304 severity: Severity::Warning,
305 category: IssueCategory::Performance,
306 code: "WASM-P003".to_string(),
307 title: "Slow WASM compilation detected".to_string(),
308 description: format!(
309 "Page render took {:?} with {} WASM modules. WASM compilation may be contributing.",
310 rendered.render_time,
311 wasm_modules.len()
312 ),
313 url: url.to_string(),
314 recommendation: "Use WebAssembly.compileStreaming() and enable WASM streaming compilation."
315 .to_string(),
316 });
317 }
318 }
319
320 let has_modulepreload = rendered.html.contains("rel=\"modulepreload\"");
322 if !wasm_modules.is_empty() && !has_modulepreload {
323 findings.push(Finding {
324 severity: Severity::Warning,
325 category: IssueCategory::Performance,
326 code: "WASM-P004".to_string(),
327 title: "Missing WASM module preload".to_string(),
328 description: "WASM modules loaded without modulepreload hint.".to_string(),
329 url: url.to_string(),
330 recommendation: "Add <link rel=\"modulepreload\"> for critical WASM modules."
331 .to_string(),
332 });
333 }
334
335 findings
336 }
337}
338
339#[cfg(test)]
344mod tests {
345 use super::*;
346 use crate::meta::MetaTags;
347 use crate::parser::{ParsedPage, ScriptInfo};
348 use crate::playwright::ConsoleMessage;
349 use std::time::Duration;
350
351 fn make_page(url: &str, script_src: &str) -> ParsedPage {
352 ParsedPage {
353 url: url.to_string(),
354 meta: MetaTags::default(),
355 headings: Vec::new(),
356 links: Vec::new(),
357 images: Vec::new(),
358 forms: Vec::new(),
359 scripts: vec![ScriptInfo {
360 src: Some(script_src.to_string()),
361 r#async: false,
362 defer: false,
363 script_type: None,
364 }],
365 styles: Vec::new(),
366 structured_data: Vec::new(),
367 word_count: 0,
368 landmarks: Vec::new(),
369 has_skip_link: false,
370 has_main_landmark: false,
371 has_nav_landmark: false,
372 has_positive_tabindex: false,
373 tabindex_negative_count: 0,
374 aria_role_count: 0,
375 aria_label_count: 0,
376 has_lang_attribute: false,
377 html_lang: None,
378 has_aria_hidden: false,
379 tables_with_headers: 0,
380 tables_total: 0,
381 tables_with_captions: 0,
382 og_image_width: None,
383 og_image_height: None,
384 }
385 }
386
387 fn default_config() -> CrawlConfig {
388 CrawlConfig::default()
389 }
390
391 fn make_ctx<'a>(page: &'a ParsedPage) -> AnalysisContext<'a> {
392 AnalysisContext {
393 page,
394 status_code: Some(200),
395 headers: &[],
396 response_time: Some(Duration::from_millis(100)),
397 redirect_chain: &[],
398 robots_txt: None,
399 }
400 }
401
402 #[test]
403 fn test_wasm_patterns_detected() {
404 let analyzer = WasmPatternAnalyzer::new();
405 let page = make_page("https://example.com", "module.wasm");
406 let ctx = make_ctx(&page);
407
408 let findings = analyzer.analyze(&ctx, &default_config());
409 assert!(
411 !findings.is_empty()
412 || page
413 .scripts
414 .iter()
415 .any(|s| s.src.as_deref().is_some_and(|src| src.contains(".wasm")))
416 );
417 }
418
419 #[test]
420 fn test_no_wasm_patterns() {
421 let analyzer = WasmPatternAnalyzer::new();
422 let page = make_page("https://example.com", "app.js");
423 let ctx = make_ctx(&page);
424
425 let findings = analyzer.analyze(&ctx, &default_config());
426 assert!(findings.iter().all(|f| !f.code.starts_with("WASM")));
428 }
429
430 #[test]
431 fn test_wasm_runtime_analyzer_console_error() {
432 let analyzer = WasmRuntimeAnalyzer::new();
433 let rendered = RenderedPage {
434 final_url: "https://example.com".to_string(),
435 html: String::new(),
436 console_messages: vec![ConsoleMessage {
437 level: "error".to_string(),
438 text: "WebAssembly.instantiate failed".to_string(),
439 source: None,
440 line: None,
441 }],
442 network_requests: Vec::new(),
443 wasm_errors: Vec::new(),
444 render_time: Duration::from_millis(100),
445 memory_used: 0,
446 };
447
448 let findings = analyzer.analyze_rendered("https://example.com", &rendered);
449 assert!(findings.iter().any(|f| f.code == "WASM-R001"));
450 }
451
452 #[test]
453 fn test_wasm_performance_analyzer_large_bundle() {
454 let analyzer = WasmPerformanceAnalyzer::new();
455 let rendered = RenderedPage {
456 final_url: "https://example.com".to_string(),
457 html: String::new(),
458 console_messages: Vec::new(),
459 network_requests: vec![crate::playwright::NetworkRequest {
460 url: "https://example.com/module.wasm".to_string(),
461 method: "GET".to_string(),
462 status: Some(200),
463 resource_type: "wasm".to_string(),
464 size: Some(15 * 1024 * 1024), }],
466 wasm_errors: Vec::new(),
467 render_time: Duration::from_millis(100),
468 memory_used: 0,
469 };
470
471 let findings = analyzer.analyze_rendered("https://example.com", &rendered);
472 assert!(findings.iter().any(|f| f.code == "WASM-P002"));
473 }
474}