Line data Source code
1 : //! A script: the text of an editor or a catalog entry and its latest scanned, parsed and analyzed
2 : //! versions.
3 : //!
4 : //! The text is a rope indexed by Unicode code points, so that edits in the middle of large scripts
5 : //! stay logarithmic.
6 : //!
7 : //! A script does not hold a reference to its catalog. Operations that read
8 : //! the catalog take it as an argument, and dropping a script does not modify the catalog: the owner
9 : //! drops the script from the catalog explicitly (`Catalog::drop_script`).
10 :
11 : use crate::analyzer::{analyze_with, AnalyzedScript};
12 : use crate::table_parameters::TableParameters;
13 : use crate::catalog::{Catalog, CatalogEntryId};
14 : use crate::completion::Completion;
15 : use crate::cursor::{ScriptCursor, ScriptView};
16 : use crate::error::{Error, Result};
17 : use crate::formatter::Formatter;
18 : use crate::vis_rewrite::{VisBoard, VisFlow, VisRenderer};
19 : use dashql_buffers::formatting::FormattingConfigT;
20 : use dashql_buffers::parser::{Node, StatementType};
21 : use dashql_buffers::statistics::{ScriptMemoryStatisticsT, ScriptProcessingTimingsT, ScriptStatisticsT};
22 : use dashql_buffers::status::StatusCode;
23 : use dashql_parser::parser::{parse, ParsedScript};
24 : use dashql_parser::scanner::{scan, ScannedScript};
25 : use dashql_parser::span::TextSpan;
26 : use ropey::Rope;
27 : use std::sync::Arc;
28 :
29 : /// The version of the script text, bumped by every edit
30 : pub type TextVersion = u64;
31 :
32 : /// The durations of the last scan, parse and analysis of a script in nanoseconds
33 : #[derive(Clone, Copy, Debug, Default, PartialEq)]
34 : pub struct ProcessingTimings {
35 : /// The duration of the last scan
36 : pub scanner_ns: f64,
37 : /// The duration of the last parse
38 : pub parser_ns: f64,
39 : /// The duration of the last analysis
40 : pub analyzer_ns: f64,
41 : }
42 :
43 : /// A script text and its latest scanned, parsed and analyzed versions
44 : pub struct Script {
45 : /// The catalog entry id
46 : catalog_entry_id: CatalogEntryId,
47 : /// The text
48 : text: Rope,
49 : /// The text version
50 : text_version: TextVersion,
51 : /// The latest scanned script
52 : pub scanned: Option<Arc<ScannedScript>>,
53 : /// The latest parsed script
54 : pub parsed: Option<Arc<ParsedScript>>,
55 : /// The latest analyzed script
56 : pub analyzed: Option<Arc<AnalyzedScript>>,
57 : /// The results of the scripts of the session that the table parameters read, the next analysis uses them
58 : pub table_parameters: Option<Arc<TableParameters>>,
59 : /// The text offset of the cursor, reset by every analysis
60 : cursor_offset: Option<usize>,
61 : /// The durations of the last scan, parse and analysis
62 : timings: ProcessingTimings,
63 : }
64 :
65 : /// A stopwatch for the processing timings. `std::time::Instant` panics on wasm32-unknown-unknown,
66 : /// where the timings stay 0 until the host provides a clock.
67 : struct Stopwatch {
68 : /// The instant the stopwatch was started
69 : #[cfg(not(target_arch = "wasm32"))]
70 : start: std::time::Instant,
71 : }
72 :
73 : impl Stopwatch {
74 7436 : fn start() -> Self {
75 7436 : Self {
76 7436 : #[cfg(not(target_arch = "wasm32"))]
77 7436 : start: std::time::Instant::now(),
78 7436 : }
79 7436 : }
80 :
81 : /// The elapsed nanoseconds
82 7436 : fn elapsed_ns(&self) -> f64 {
83 : #[cfg(not(target_arch = "wasm32"))]
84 : {
85 7436 : self.start.elapsed().as_nanos() as f64
86 : }
87 : #[cfg(target_arch = "wasm32")]
88 : {
89 : 0.0
90 : }
91 7436 : }
92 : }
93 :
94 : impl Script {
95 : /// Create a script with a new entry id of the catalog
96 2992 : pub fn new(catalog: &Catalog) -> Self {
97 2992 : Self::with_entry_id(catalog.allocate_entry_id())
98 2992 : }
99 :
100 2993 : pub fn with_entry_id(catalog_entry_id: CatalogEntryId) -> Self {
101 2993 : Self {
102 2993 : catalog_entry_id,
103 2993 : text: Rope::new(),
104 2993 : text_version: 0,
105 2993 : scanned: None,
106 2993 : parsed: None,
107 2993 : analyzed: None,
108 2993 : table_parameters: None,
109 2993 : cursor_offset: None,
110 2993 : timings: ProcessingTimings::default(),
111 2993 : }
112 2993 : }
113 :
114 4188 : pub fn catalog_entry_id(&self) -> CatalogEntryId {
115 4188 : self.catalog_entry_id
116 4188 : }
117 :
118 1732 : pub fn text_version(&self) -> TextVersion {
119 1732 : self.text_version
120 1732 : }
121 :
122 415 : pub fn text(&self) -> &Rope {
123 415 : &self.text
124 415 : }
125 :
126 : /// Overwrite the text version. Only for tests that need versions beyond 32 bits.
127 : #[doc(hidden)]
128 1 : pub fn set_text_version(&mut self, text_version: TextVersion) {
129 1 : self.text_version = text_version;
130 1 : }
131 :
132 : /// Replace the text with an edited rope and bump the version. Unlike `replace_text`, the texts
133 : /// are not compared: the caller knows that the text changed.
134 83 : pub fn set_text(&mut self, text: Rope) {
135 83 : self.text = text;
136 83 : self.text_version += 1;
137 83 : }
138 :
139 : /// Insert text at a code point offset. Offsets past the end append.
140 541 : pub fn insert_text_at(&mut self, char_idx: usize, text: &str) {
141 541 : if text.is_empty() {
142 2 : return;
143 539 : }
144 539 : let char_idx = char_idx.min(self.text.len_chars());
145 539 : self.text.insert(char_idx, text);
146 539 : self.text_version += 1;
147 541 : }
148 :
149 : /// Erase a code point range
150 51 : pub fn erase_text_range(&mut self, char_idx: usize, count: usize) {
151 51 : let len = self.text.len_chars();
152 51 : if count == 0 || char_idx >= len {
153 4 : return;
154 47 : }
155 47 : let end = (char_idx + count).min(len);
156 47 : self.text.remove(char_idx..end);
157 47 : self.text_version += 1;
158 51 : }
159 :
160 : /// Replace the text, a no-op if the text is unchanged
161 1620 : pub fn replace_text(&mut self, text: &str) {
162 1620 : if text.len() == self.text.len_bytes() && self.text == text {
163 10 : return;
164 1610 : }
165 1610 : self.text = Rope::from_str(text);
166 1610 : self.text_version += 1;
167 1620 : }
168 :
169 : /// The text as a string
170 4033 : pub fn to_text(&self) -> String {
171 4033 : self.text.to_string()
172 4033 : }
173 :
174 : /// The text of a byte span, empty if the span begins past the end. The span
175 : /// is clamped to the text; characters cut by the span bounds become replacement characters.
176 5 : pub fn to_text_span(&self, span: TextSpan) -> String {
177 5 : let len = self.text.len_bytes();
178 5 : let begin = span.offset as usize;
179 5 : if begin >= len {
180 1 : return String::new();
181 4 : }
182 4 : let end = (begin + span.length as usize).min(len);
183 4 : let bytes: Vec<u8> = self.text.bytes_at(begin).take(end - begin).collect();
184 4 : String::from_utf8_lossy(&bytes).into_owned()
185 5 : }
186 :
187 : /// Scan the script unconditionally
188 2184 : pub fn scan(&mut self) {
189 2184 : let watch = Stopwatch::start();
190 2184 : let text = self.text.to_string();
191 2184 : self.scanned = Some(Arc::new(scan(&text, self.text_version, self.catalog_entry_id)));
192 2184 : self.timings.scanner_ns = watch.elapsed_ns();
193 2184 : }
194 :
195 : /// Parse the script unconditionally, scanning first if the scan is missing or outdated
196 2179 : pub fn parse(&mut self) {
197 2179 : if self.scanned.as_ref().is_none_or(|s| s.text_version != self.text_version) {
198 483 : self.scan();
199 2085 : }
200 : // The parser registers keyword names and the replacements of obfuscated literals in a copy
201 : // of the scanned script (in place if it is not shared), which becomes the scanned script
202 2179 : let watch = Stopwatch::start();
203 2179 : let scanned = self.scanned.take().unwrap();
204 2179 : let parsed = parse(scanned);
205 2179 : self.scanned = Some(parsed.scanned.clone());
206 2179 : self.parsed = Some(Arc::new(parsed));
207 2179 : self.timings.parser_ns = watch.elapsed_ns();
208 2179 : }
209 :
210 : /// Scan and parse if the text changed
211 882 : fn parse_if_outdated(&mut self) {
212 882 : if self.scanned.as_ref().is_none_or(|s| s.text_version != self.text_version) {
213 403 : self.scan();
214 763 : }
215 882 : let current = match (&self.parsed, &self.scanned) {
216 531 : (Some(p), Some(s)) => Arc::ptr_eq(&p.scanned, s),
217 351 : _ => false,
218 : };
219 882 : if !current {
220 403 : self.parse();
221 763 : }
222 882 : }
223 :
224 : /// Parse if the parsed script is missing or was parsed from another text version (the check of
225 : /// `statement_text`, `rewrite_vis_board` and `compute_signature`)
226 13 : fn parse_if_text_outdated(&mut self) {
227 13 : if self.parsed.as_ref().is_none_or(|p| p.scanned.text_version != self.text_version) {
228 12 : self.parse();
229 12 : }
230 13 : }
231 :
232 : /// Analyze the script
233 3075 : pub fn analyze(&mut self, catalog: &Catalog, parse_if_outdated: bool) -> Result<()> {
234 3075 : if parse_if_outdated {
235 876 : self.parse_if_outdated();
236 3070 : }
237 3075 : let Some(parsed) = self.parsed.clone() else {
238 2 : return Err(Error::status(StatusCode::SCRIPT_NOT_PARSED));
239 : };
240 3073 : self.cursor_offset = None;
241 3073 : let watch = Stopwatch::start();
242 3073 : self.analyzed = Some(Arc::new(analyze_with(parsed, catalog, self.table_parameters.as_ref())?));
243 3073 : self.timings.analyzer_ns = watch.elapsed_ns();
244 3073 : Ok(())
245 3075 : }
246 :
247 : /// The scanned, parsed and analyzed script if the analysis is current with the text, whatever catalog it was
248 : /// analyzed with
249 5450 : pub fn current_analysis(&self) -> Option<(&Arc<ScannedScript>, &Arc<ParsedScript>, &Arc<AnalyzedScript>)> {
250 5450 : let (scanned, parsed, analyzed) = (self.scanned.as_ref()?, self.parsed.as_ref()?, self.analyzed.as_ref()?);
251 5200 : let current = Arc::ptr_eq(&analyzed.parsed, parsed) && Arc::ptr_eq(&parsed.scanned, scanned) && scanned.text_version == self.text_version;
252 5200 : current.then_some((scanned, parsed, analyzed))
253 5450 : }
254 :
255 : /// Is the analysis current with the script results of the script? An analysis without table parameters reads
256 : /// no results, any other was analyzed with the same results.
257 122 : pub fn table_parameters_current(&self) -> bool {
258 122 : let Some(analyzed) = self.analyzed.as_ref() else { return true };
259 122 : if !analyzed.parameters.iter().any(|p| p.kind == crate::analyzer::ParameterKind::Table) {
260 110 : return true;
261 12 : }
262 12 : match (analyzed.table_parameters.as_ref(), self.table_parameters.as_ref()) {
263 0 : (None, None) => true,
264 10 : (Some(l), Some(r)) => Arc::ptr_eq(l, r),
265 2 : _ => false,
266 : }
267 122 : }
268 :
269 : /// The parts of the script that a cursor reads
270 362 : pub fn view<'a>(&'a self, catalog: &'a Catalog) -> ScriptView<'a> {
271 362 : ScriptView {
272 362 : scanned: self.scanned.as_ref(),
273 362 : parsed: self.parsed.as_ref(),
274 362 : analyzed: self.analyzed.as_ref(),
275 362 : catalog,
276 362 : }
277 362 : }
278 :
279 : /// Move the cursor to a text offset
280 327 : pub fn move_cursor<'a>(&'a mut self, catalog: &'a Catalog, text_offset: usize) -> ScriptCursor<'a> {
281 327 : self.cursor_offset = Some(text_offset);
282 327 : ScriptCursor::place(self.view(catalog), text_offset)
283 327 : }
284 :
285 : /// The text offset of the cursor, if it was moved since the last analysis
286 44 : pub fn cursor_offset(&self) -> Option<usize> {
287 44 : self.cursor_offset
288 44 : }
289 :
290 : /// Move or reset the cursor without placing it. The cursor is placed when it is read with
291 : /// `cursor`.
292 154 : pub fn set_cursor_offset(&mut self, text_offset: Option<usize>) {
293 154 : self.cursor_offset = text_offset;
294 154 : }
295 :
296 : /// The cursor, if it was moved since the last analysis
297 35 : pub fn cursor<'a>(&'a self, catalog: &'a Catalog) -> Option<ScriptCursor<'a>> {
298 35 : self.cursor_offset.map(|offset| ScriptCursor::place(self.view(catalog), offset))
299 35 : }
300 :
301 : /// Complete at the cursor
302 316 : pub fn complete_at_cursor<'c, 'a>(cursor: &'c ScriptCursor<'a>, limit: usize) -> Result<Completion<'c, 'a>> {
303 316 : if cursor.scanner_location.is_none() {
304 0 : return Err(Error::status(StatusCode::COMPLETION_MISSES_SCANNER_TOKEN));
305 316 : }
306 316 : Ok(Completion::compute(cursor, limit))
307 316 : }
308 :
309 : /// Compute the signature
310 4 : pub fn compute_signature(&mut self, parse_if_outdated: bool) -> Result<String> {
311 4 : if parse_if_outdated {
312 2 : self.parse_if_text_outdated();
313 2 : }
314 4 : let parsed = self.parsed.as_ref().ok_or_else(|| Error::status(StatusCode::SCRIPT_NOT_PARSED))?;
315 3 : crate::signature::compute_signature(parsed)
316 4 : }
317 :
318 : /// The text of the first statement without its separator or surrounding trivia
319 6 : pub fn statement_text(&mut self, parse_if_outdated: bool) -> Result<String> {
320 6 : if parse_if_outdated {
321 2 : self.parse_if_text_outdated();
322 5 : }
323 6 : let parsed = self.parsed.as_ref().ok_or_else(|| Error::status(StatusCode::SCRIPT_NOT_PARSED))?;
324 5 : if parsed.statements.is_empty() {
325 2 : return Ok(String::new());
326 3 : }
327 3 : let span = parsed.associate_descriptions()[0].statement_span;
328 3 : Ok(self.to_text_span(span))
329 6 : }
330 :
331 : /// The parsed script whose spans apply to the current text, parsed first if requested and outdated
332 86 : fn parsed_for_rewrite(&mut self, parse_if_outdated: bool) -> Result<Arc<ParsedScript>> {
333 86 : if parse_if_outdated {
334 9 : self.parse_if_text_outdated();
335 77 : }
336 86 : self.parsed.clone().ok_or_else(|| Error::status(StatusCode::SCRIPT_NOT_PARSED))
337 86 : }
338 :
339 : /// The annotation of a statement: the comment tags in front of it (see `annotation`)
340 6 : pub fn read_annotation(&mut self, statement_id: usize, parse_if_outdated: bool) -> Result<crate::annotation::Annotation> {
341 6 : let parsed = self.parsed_for_rewrite(parse_if_outdated)?;
342 5 : crate::annotation::read_annotation(&parsed, statement_id)
343 6 : }
344 :
345 : /// Replace the annotation of a statement, printed as one comment in place of its tagged comments. An
346 : /// empty annotation removes them. Returns the new text, the script is not changed.
347 : ///
348 : /// The spans of the parsed script are applied to the current text, which differs from the parsed
349 : /// text if the script was edited and `parse_if_outdated` is false (see docs/design/core_quirks.md).
350 3 : pub fn rewrite_annotation(
351 3 : &mut self,
352 3 : statement_id: usize,
353 3 : annotation: &crate::annotation::Annotation,
354 3 : parse_if_outdated: bool,
355 3 : ) -> Result<String> {
356 3 : let parsed = self.parsed_for_rewrite(parse_if_outdated)?;
357 3 : crate::annotation::write_annotation(&self.text.to_string(), &parsed, statement_id, annotation)
358 3 : }
359 :
360 : /// Set or remove the renderer of a query, its `@vis vegalite`, `markdown` or `table` tag (see
361 : /// `vis_rewrite`). Returns the new text, the script is not changed.
362 : ///
363 : /// The spans of the parsed script are applied to the current text, which differs from the parsed
364 : /// text if the script was edited and `parse_if_outdated` is false (see docs/design/core_quirks.md).
365 29 : pub fn rewrite_vis_renderer(
366 29 : &mut self,
367 29 : statement_id: usize,
368 29 : renderer: Option<&VisRenderer>,
369 29 : parse_if_outdated: bool,
370 29 : ) -> Result<String> {
371 29 : let parsed = self.parsed_for_rewrite(parse_if_outdated)?;
372 28 : crate::vis_rewrite::rewrite_vis_renderer(&self.text.to_string(), &parsed, statement_id, renderer)
373 29 : }
374 :
375 : /// Set or remove the dashboard placement of a statement, its `@board` tag. Returns the new text like
376 : /// `rewrite_vis_renderer`.
377 30 : pub fn rewrite_vis_board(&mut self, statement_id: usize, board: Option<&VisBoard>, parse_if_outdated: bool) -> Result<String> {
378 30 : let parsed = self.parsed_for_rewrite(parse_if_outdated)?;
379 30 : crate::vis_rewrite::rewrite_vis_board(&self.text.to_string(), &parsed, statement_id, board)
380 30 : }
381 :
382 : /// Set or remove the position of the script on the dataflow canvas, its `@flow` tag. Returns the new
383 : /// text like `rewrite_vis_renderer`.
384 11 : pub fn rewrite_vis_flow(&mut self, flow: Option<&VisFlow>, parse_if_outdated: bool) -> Result<String> {
385 11 : let parsed = self.parsed_for_rewrite(parse_if_outdated)?;
386 11 : crate::vis_rewrite::rewrite_vis_flow(&self.text.to_string(), &parsed, flow)
387 11 : }
388 :
389 : /// Set or remove the value of an input of the script, its `@param` tag. A new tag goes to the statement that uses
390 : /// the input first. Returns the new text like `rewrite_vis_renderer`.
391 7 : pub fn rewrite_param_tag(&mut self, name: &str, value: Option<&str>, parse_if_outdated: bool) -> Result<String> {
392 7 : let parsed = self.parsed_for_rewrite(parse_if_outdated)?;
393 6 : let first_use = self
394 6 : .analyzed
395 6 : .as_ref()
396 6 : .filter(|a| Arc::ptr_eq(&a.parsed, &parsed))
397 6 : .and_then(|a| {
398 2 : a.parameters
399 2 : .iter()
400 2 : .find(|p| p.kind == crate::analyzer::ParameterKind::Scalar && p.name.as_deref() == Some(name))
401 2 : .and_then(|p| p.ast_statement_id)
402 2 : })
403 6 : .map(|s| s as usize);
404 6 : crate::param_rewrite::rewrite_param_tag(&self.text.to_string(), &parsed, name, value, first_use)
405 7 : }
406 :
407 : /// Visualize the terminal query with a Vega-Lite spec: replace its renderer tag (`@vis vegalite`,
408 : /// `markdown` or `table`) by `@vis vegalite <spec>`, or add one. The annotation of the query is
409 : /// consolidated into one comment (see `vis_rewrite`).
410 : ///
411 : /// The spec must be a JSON object, it is written compactly with its key order kept (`compact_json`).
412 : /// `/*` and `*/` in its strings are escaped as `/\u002a` and `*\/`, they would nest or end a block comment.
413 : /// The spans of the parsed script are applied to the current text, as in `rewrite_vis_board`.
414 : ///
415 : /// Returns the rewritten text and the span of the written tag in it, from `@vis` to the end of the spec.
416 29 : pub fn rewrite_vegalite(&mut self, spec: &str, parse_if_outdated: bool) -> Result<(String, TextSpan)> {
417 29 : if parse_if_outdated {
418 0 : self.parse_if_text_outdated();
419 29 : }
420 29 : let not_parsed = || Error::status(StatusCode::SCRIPT_NOT_PARSED);
421 29 : let parsed = self.parsed.clone().ok_or_else(not_parsed)?;
422 29 : if !parsed.errors.is_empty() || parsed.statements.is_empty() {
423 2 : return Err(not_parsed());
424 27 : }
425 27 : let statement_id = parsed.statements.len() - 1;
426 27 : if parsed.statements[statement_id].statement_type != StatementType::SELECT {
427 1 : return Err(Error::invalid_argument("the last statement of the script is no query"));
428 26 : }
429 26 : let spec = match serde_json::from_str(spec) {
430 24 : Ok(serde_json::Value::Object(spec)) => spec,
431 1 : Ok(_) => return Err(Error::invalid_argument("the Vega-Lite spec is no JSON object")),
432 1 : Err(e) => return Err(Error::invalid_argument(format!("the Vega-Lite spec is no valid JSON: {e}"))),
433 : };
434 24 : let renderer = VisRenderer::VegaLite(spec);
435 24 : let (text, span) = crate::vis_rewrite::rewrite_vis_renderer_tag(&self.text.to_string(), &parsed, statement_id, Some(&renderer))?;
436 24 : Ok((text, span.expect("a written tag has a span")))
437 29 : }
438 :
439 : /// Scan and parse if requested and outdated, then return the parsed script to format
440 13 : fn parsed_for_formatting(&mut self, parse_if_outdated: bool) -> Result<&Arc<ParsedScript>> {
441 13 : if parse_if_outdated {
442 6 : self.parse_if_outdated();
443 9 : }
444 13 : self.parsed.as_ref().ok_or_else(|| Error::status(StatusCode::SCRIPT_NOT_PARSED))
445 13 : }
446 :
447 : /// Format the script
448 5 : pub fn format(&mut self, config: &FormattingConfigT, parse_if_outdated: bool) -> Result<String> {
449 5 : let parsed = self.parsed_for_formatting(parse_if_outdated)?;
450 4 : Ok(Formatter::new(parsed).format(config))
451 5 : }
452 :
453 : /// Format the script and return the raw bytes, which may contain partial UTF-8 sequences where
454 : /// comments were wrapped (see `Formatter::format_bytes`)
455 2 : pub fn format_bytes(&mut self, config: &FormattingConfigT, parse_if_outdated: bool) -> Result<Vec<u8>> {
456 2 : let parsed = self.parsed_for_formatting(parse_if_outdated)?;
457 2 : Ok(Formatter::new(parsed).format_bytes(config))
458 2 : }
459 :
460 : /// The nodes that formatting could not represent
461 2 : pub fn unformattable_nodes(&mut self, config: &FormattingConfigT, parse_if_outdated: bool) -> Result<Vec<u32>> {
462 2 : let parsed = self.parsed_for_formatting(parse_if_outdated)?;
463 2 : let mut formatter = Formatter::new(parsed);
464 2 : formatter.format_bytes(config);
465 2 : Ok(formatter.unformattable_nodes())
466 2 : }
467 :
468 : /// Can formatting represent every node and does the script parse without errors?
469 : ///
470 : /// Statements with syntax errors are copied as written, which is not formatting them.
471 4 : pub fn is_fully_formattable(&mut self, config: &FormattingConfigT, parse_if_outdated: bool) -> Result<bool> {
472 4 : let parsed = self.parsed_for_formatting(parse_if_outdated)?;
473 4 : let mut formatter = Formatter::new(parsed);
474 4 : formatter.format_bytes(config);
475 4 : Ok(formatter.is_fully_formatted())
476 4 : }
477 :
478 : /// The durations of the last scan, parse and analysis in nanoseconds
479 99 : pub fn timings(&self) -> ProcessingTimings {
480 99 : self.timings
481 99 : }
482 :
483 : /// The memory statistics.
484 : ///
485 : /// They describe the latest analyzed script and the parsed and scanned script it was analyzed
486 : /// from, and are 0 except for the rope size before the first analysis. The rope and input sizes
487 : /// are text lengths, the AST size is the node count times the node size, and the name index size
488 : /// is its entry count. The others measure the Rust data structures:
489 : /// - `scanner_symbol_bytes` is the size of the symbol vector.
490 : /// - `scanner_name_dictionary_bytes` counts the registered names and their texts.
491 : /// - `analyzer_description_bytes` multiplies the element counts of the declarations, references,
492 : /// expressions and scopes with the sizes of their types.
493 : /// - `analyzer_name_index_bytes` is the size of the suffix vector.
494 101 : pub fn memory_statistics(&self) -> ScriptMemoryStatisticsT {
495 : use std::mem::size_of;
496 101 : let mut memory = ScriptMemoryStatisticsT::default();
497 101 : memory.rope_bytes = self.text.len_bytes() as u32;
498 101 : let Some(analyzed) = self.analyzed.as_ref() else {
499 2 : return memory;
500 : };
501 99 : let entry = &analyzed.entry;
502 99 : let table_column_bytes: usize = entry
503 99 : .table_declarations
504 99 : .iter()
505 99 : .map(|t| t.table_columns.len() * size_of::<crate::catalog::TableColumn>())
506 99 : .sum();
507 99 : let description_bytes = entry.database_references.len() * size_of::<crate::catalog::DatabaseReference>()
508 99 : + entry.schema_references.len() * size_of::<crate::catalog::SchemaReference>()
509 99 : + entry.table_declarations.len() * size_of::<crate::catalog::TableDeclaration>()
510 99 : + table_column_bytes
511 99 : + analyzed.table_references.len() * size_of::<crate::analyzer::TableReference>()
512 99 : + analyzed.expressions.len() * size_of::<crate::analyzer::Expression>()
513 99 : + analyzed.function_arguments.len() * size_of::<crate::analyzer::FunctionArgument>()
514 99 : + analyzed.name_scopes.len() * size_of::<crate::analyzer::NameScope>();
515 99 : let latest = &mut memory.latest_script;
516 99 : latest.analyzer_description_bytes = description_bytes as u32;
517 99 : let index = analyzed.name_search_index();
518 99 : if index.is_built() {
519 1 : latest.analyzer_name_index_size = index.len() as u32;
520 1 : latest.analyzer_name_index_bytes = index.byte_size() as u32;
521 98 : }
522 99 : let parsed = &analyzed.parsed;
523 99 : latest.parser_ast_bytes = (parsed.nodes.len() * size_of::<Node>()) as u32;
524 99 : let scanned = &parsed.scanned;
525 99 : latest.scanner_input_bytes = scanned.text.len() as u32;
526 99 : latest.scanner_symbol_bytes = (scanned.symbols.len() * size_of::<dashql_parser::scanner::Symbol>()) as u32;
527 99 : let name_bytes: usize = scanned
528 99 : .names
529 99 : .iter()
530 171 : .map(|n| n.text.len() + size_of::<dashql_parser::names::RegisteredName>())
531 99 : .sum();
532 99 : latest.scanner_name_dictionary_bytes = name_bytes as u32;
533 99 : memory
534 101 : }
535 :
536 : /// The processing statistics
537 2 : pub fn statistics(&self) -> ScriptStatisticsT {
538 2 : let mut statistics = ScriptStatisticsT::default();
539 2 : let mut timings = ScriptProcessingTimingsT::default();
540 2 : timings.scanner_last_elapsed = self.timings.scanner_ns;
541 2 : timings.parser_last_elapsed = self.timings.parser_ns;
542 2 : timings.analyzer_last_elapsed = self.timings.analyzer_ns;
543 2 : statistics.timings = Some(timings);
544 2 : statistics.memory = Some(self.memory_statistics());
545 2 : statistics
546 2 : }
547 : }
|