evobench_tools/run/output_directory/
html_files.rs

1//! Generate the HTML files at the top of the output directory for
2//! easy access of the outputs. This uses the same code as the
3//! `evobench list` subcommand, and some more.
4
5use std::{
6    borrow::Cow,
7    collections::{BTreeMap, BTreeSet, btree_map::Entry},
8    io::Write,
9    sync::Arc,
10};
11
12use ahtml::{ASlice, HtmlAllocator, Node, SerHtmlFrag};
13use anyhow::Result;
14use kstring::KString;
15
16use crate::{
17    io_utils::tempfile_utils::tempfile,
18    output_table::{CellValue, OutputTable, OutputTableTitle, html::HtmlTable},
19    run::{
20        config::{RunConfig, ShareableConfig},
21        output_directory::structure::{ParametersDir, ToPath},
22        run_queues::RunQueues,
23        sub_command::list::{OutputTableOpts, ParameterView},
24        working_directory_pool::WorkingDirectoryPoolBaseDir,
25    },
26    utillib::{arc::CloneArc, into_arc_path::IntoArcPath, invert_index::invert_index_by_ref},
27};
28
29fn print_html_document(
30    body: ASlice<Node>,
31    html: &HtmlAllocator,
32    mut out: impl Write,
33) -> Result<()> {
34    // If a fragment was given, and it starts with "end-", scroll so
35    // that it lines up at the *end* of the viewport. Sadly without JS
36    // it will line up at the top; todo: put anchor (with a different
37    // name) for non-JS use in the 30th-last row or so instead?
38    let code: Arc<str> = Arc::from(
39        r##"
40<script>
41  if (location.hash) {
42    history.scrollRestoration = "manual";
43  }
44
45  window.addEventListener("DOMContentLoaded", () => {
46    if (location.hash.length > 0) {
47      const id = location.hash.substring(1);
48      const el = document.getElementById(id);
49      if (el) {
50        if (id.startsWith("end-")) {
51          el.scrollIntoView({ block: "end" });
52        } else {
53          el.scrollIntoView();
54        }
55      }
56    }
57  });
58</script>
59"##,
60    );
61
62    let doc = html.html(
63        [],
64        [
65            html.head(
66                [],
67                html.preserialized(SerHtmlFrag {
68                    meta: &ahtml::SCRIPT_META,
69                    string: code,
70                })?,
71            )?,
72            html.body([], html.table([], body)?)?,
73        ],
74    )?;
75    html.print_html_document(doc, &mut out)?;
76    out.flush()?;
77    Ok(())
78}
79
80// It's a bit of a mess: creating the table is in sub_command/list.rs,
81// we get the OutputTableOpts and associated creation op from
82// there. Currently. (I.e. it comes here from there and calls back to
83// there .)
84
85pub fn print_list(
86    conf: &RunConfig,
87    working_directory_base_dir: &Arc<WorkingDirectoryPoolBaseDir>,
88    queues: &RunQueues,
89    output_table_opts: &OutputTableOpts,
90    html: Option<&HtmlAllocator>,
91    link_skipped: Option<&str>,
92    out: impl Write,
93) -> Result<()> {
94    let tmp;
95    let html = if let Some(html) = html {
96        html
97    } else {
98        tmp = HtmlAllocator::new(1000000, Arc::new("list"));
99        &tmp
100    };
101    let num_columns = output_table_opts
102        .parameter_view
103        .unwrap_or_default()
104        .titles()
105        .len();
106    let table = HtmlTable::new(num_columns, &html);
107    let body = output_table_opts.output_to_table(
108        table,
109        conf,
110        link_skipped,
111        working_directory_base_dir,
112        queues,
113    )?;
114    print_html_document(body.as_slice(), html, out)
115}
116
117fn write_2_column_table_file<'url, T1: CellValue<'url>, T2: CellValue<'url>>(
118    file_name: &str,
119    titles: &[&str],
120    index: &BTreeMap<T1, BTreeSet<T2>>,
121    conf: &RunConfig,
122    html: &HtmlAllocator,
123) -> Result<()> {
124    // let title_style = Some(OutputStyle {
125    //     font_size: Some(FontSize::Large),
126    //     ..Default::default()
127    // });
128
129    let titles: Vec<_> = titles
130        .iter()
131        .map(|title| OutputTableTitle {
132            text: (*title).into(),
133            span: 1,
134            anchor_name: None,
135        })
136        .collect();
137
138    let (tmp_file, out) = tempfile(conf.output_dir.path.join(file_name), false)?;
139    let num_columns = titles.len();
140    let mut table = HtmlTable::new(num_columns, &html);
141    table.write_title_row(&titles, None)?;
142
143    for (k, vs) in index {
144        // let mut items = html.new_vec();
145        // for v in vs {
146        //     items.push(html.text(v.as_ref())?)?;
147        //     items.push(html.br([],[])?)?;
148        // }
149
150        // Show k once only, with the first v
151        let mut vs = vs.iter();
152        let v = vs
153            .next()
154            .expect("only adding BtreeSet with a value and never removing values");
155        let row: &[&dyn CellValue] = &[k, v];
156        table.write_data_row(row, None)?;
157
158        for v in vs {
159            let row: &[&dyn CellValue] = &[&"", v];
160            table.write_data_row(row, None)?;
161        }
162    }
163
164    print_html_document(table.finish()?.as_slice(), &html, out)?;
165    tmp_file.finish()?;
166    Ok(())
167}
168
169#[derive(PartialEq, Eq, PartialOrd, Ord)]
170struct ParametersCellValue {
171    dir: ParametersDir,
172    s: String,
173}
174
175impl From<ParametersDir> for ParametersCellValue {
176    fn from(dir: ParametersDir) -> Self {
177        let s = format!(
178            "{} -> {}",
179            dir.target_name().as_str(),
180            dir.custom_parameters()
181        );
182        Self { dir, s }
183    }
184}
185
186impl AsRef<str> for ParametersCellValue {
187    fn as_ref(&self) -> &str {
188        &self.s
189    }
190}
191
192impl<'url> CellValue<'url> for ParametersCellValue {
193    fn perhaps_url(&self) -> Option<Cow<'static, str>> {
194        Some(self.dir.to_path().to_string_lossy().to_string().into())
195    }
196    fn perhaps_anchor_name(&self) -> Option<&KString> {
197        None
198    }
199}
200
201impl<'url> CellValue<'url> for &ParametersCellValue {
202    fn perhaps_url(&self) -> Option<Cow<'static, str>> {
203        Some(self.dir.to_path().to_string_lossy().to_string().into())
204    }
205    fn perhaps_anchor_name(&self) -> Option<&KString> {
206        None
207    }
208}
209
210impl<'url> CellValue<'url> for KString {
211    fn perhaps_url(&self) -> Option<Cow<'static, str>> {
212        None
213    }
214    fn perhaps_anchor_name(&self) -> Option<&KString> {
215        None
216    }
217}
218
219impl<'url> CellValue<'url> for &KString {
220    fn perhaps_url(&self) -> Option<Cow<'static, str>> {
221        None
222    }
223    fn perhaps_anchor_name(&self) -> Option<&KString> {
224        None
225    }
226}
227
228/// Does not take a lock: just regenerates the file (via
229/// tempfile-rename) with external values at least from now. For
230/// savings, pass the optional values if you can.
231pub fn regenerate_index_files(
232    shareable_config: &ShareableConfig,
233    working_directory_base_dir: Option<&Arc<WorkingDirectoryPoolBaseDir>>,
234    queues: Option<&RunQueues>,
235) -> Result<()> {
236    let conf = &shareable_config.run_config;
237
238    // Copies from src/bin/evobench.rs; hacky.
239
240    let tmp;
241    let working_directory_base_dir = if let Some(d) = working_directory_base_dir {
242        d
243    } else {
244        tmp = Arc::new(WorkingDirectoryPoolBaseDir::new(
245            conf.working_directory_pool.base_dir.clone(),
246            &|| {
247                shareable_config
248                    .global_app_state_dir
249                    .working_directory_pool_base()
250            },
251        )?);
252        &tmp
253    };
254
255    let tmp2;
256    let queues = if let Some(q) = queues {
257        q
258    } else {
259        tmp2 = RunQueues::open(
260            shareable_config.run_config.queues.clone_arc(),
261            true,
262            &shareable_config.global_app_state_dir,
263            // No need to signal changes, not going to mutate anything
264            None,
265        )?;
266        &tmp2
267    };
268
269    // / setup
270
271    let mut html = HtmlAllocator::new(1000000, Arc::new("regenerate_index_files"));
272
273    let write_jobs_list =
274        |html: &HtmlAllocator, file_name: &str, all: bool, link: Option<&str>| -> Result<()> {
275            let output_table_opts = OutputTableOpts {
276                verbose: false,
277                all,
278                n: None,
279                parameter_view: Some(ParameterView::Separated),
280            };
281
282            let (tmp_file, out) = tempfile(conf.output_dir.path.join(file_name), false)?;
283
284            print_list(
285                conf,
286                working_directory_base_dir,
287                queues,
288                &output_table_opts,
289                Some(html),
290                link,
291                out,
292            )?;
293
294            tmp_file.finish()?;
295
296            Ok(())
297        };
298
299    // Write the jobs list with the default limit
300    write_jobs_list(&html, "list.html", false, Some("list-unlimited.html"))?;
301    html.clear();
302    // And again with "all" jobs; to avoid confusion, do not use
303    // "list-all.html" since "list-all" is a different evobench
304    // subcommand.
305    write_jobs_list(&html, "list-unlimited.html", true, None)?;
306    html.clear();
307
308    // parameter lists
309    if let Some(base_url) = &conf.output_dir.url {
310        let paths_with_names = {
311            let mut paths_with_names = BTreeMap::new();
312            for (name, templates) in &conf.job_template_lists {
313                for template in &**templates {
314                    let dir = ParametersCellValue::from(
315                        template.to_parameters_dir(base_url.into_arc_path()),
316                    );
317                    match paths_with_names.entry(dir) {
318                        Entry::Vacant(vacant_entry) => {
319                            let mut m = BTreeSet::new();
320                            m.insert(name.clone());
321                            vacant_entry.insert(m);
322                        }
323                        Entry::Occupied(mut occupied_entry) => {
324                            occupied_entry.get_mut().insert(name.clone());
325                        }
326                    }
327                }
328            }
329            paths_with_names
330        };
331
332        write_2_column_table_file(
333            "by_parameters.html",
334            &["Parameter", "Templates names"],
335            &paths_with_names,
336            conf,
337            &html,
338        )?;
339        html.clear();
340
341        let names_with_paths = invert_index_by_ref(&paths_with_names);
342        write_2_column_table_file(
343            "by_templates_name.html",
344            &["Templates name", "Parameters"],
345            &names_with_paths,
346            conf,
347            &html,
348        )?;
349        html.clear();
350    }
351
352    Ok(())
353}