oxidd_dump/
visualize.rs

1//! Visualization with [OxiDD-vis](https://oxidd.net/vis)
2
3use std::fmt;
4use std::io::{self, Read, Write};
5use std::net::{TcpListener, TcpStream};
6
7use oxidd_core::function::{Function, INodeOfFunc, TermOfFunc};
8use oxidd_core::HasLevel;
9
10use crate::AsciiDisplay;
11
12/// [OxiDD-vis]-compatible decision diagram exporter that
13/// serves decision diagram dumps on localhost via HTTP
14///
15/// [OxiDD-vis] is a webapp that runs locally in your browser. You can directly
16/// send decision diagrams to it via an HTTP connection. Here, the `Visualizer`
17/// acts as a small HTTP server that accepts connections once you call
18/// [`Visualizer::serve()`]. The webapp repeatedly polls on the configured port
19/// to directly display the decision diagrams then.
20///
21/// [OxiDD-vis]: https://oxidd.net/vis
22pub struct Visualizer {
23    port: u16,
24    buf: Vec<u8>,
25}
26
27impl Default for Visualizer {
28    #[inline]
29    fn default() -> Self {
30        Self::new()
31    }
32}
33
34impl Visualizer {
35    /// Create a new visualizer
36    pub fn new() -> Self {
37        Self {
38            port: 4000,
39            buf: Vec::with_capacity(2 * 1024 * 1024), // 2 MiB
40        }
41    }
42
43    /// Customize the port on which to serve the visualization data
44    ///
45    /// The default port is 4000.
46    ///
47    /// Returns `self`.
48    #[inline(always)]
49    pub fn port(mut self, port: u16) -> Self {
50        self.port = port;
51        self
52    }
53
54    fn add_diagram_start(&mut self, ty: &str, diagram_name: &str) {
55        let buf = &mut self.buf;
56        if !buf.is_empty() {
57            buf.push(b',');
58        }
59
60        buf.extend_from_slice(b"{\"type\":\"");
61        buf.extend_from_slice(ty.as_bytes());
62        buf.extend_from_slice(b"\",\"name\":\"");
63        json_escape(buf, diagram_name.as_bytes());
64        buf.extend_from_slice(b"\",\"diagram\":\"");
65    }
66
67    /// Add a decision diagram for visualization
68    ///
69    /// `diagram_name` can be used as an identifier in case you add multiple
70    /// diagrams. `functions` is an iterator over [`Function`]s.
71    ///
72    /// The visualization includes all nodes reachable from the root nodes
73    /// referenced by `functions`. If you wish to name the functions, use
74    /// [`Self::add_with_names()`].
75    ///
76    /// Returns `self` to allow chaining.
77    ///
78    /// # Example
79    ///
80    /// ```
81    /// # use oxidd_core::function::Function;
82    /// # use oxidd_dump::Visualizer;
83    /// # fn vis<'id, F: Function>(manager: &F::Manager<'id>, f0: &F, f1: &F)
84    /// # where
85    /// #    oxidd_core::function::INodeOfFunc<'id, F>: oxidd_core::HasLevel,
86    /// #    oxidd_core::function::TermOfFunc<'id, F>: oxidd_dump::AsciiDisplay,
87    /// # {
88    /// Visualizer::new()
89    ///     .add("my_diagram", manager, [f0, f1])
90    ///     .serve()
91    ///     .expect("failed to serve diagram due to I/O error")
92    /// # }
93    /// ```
94    pub fn add<'id, FR: std::ops::Deref>(
95        mut self,
96        diagram_name: &str,
97        manager: &<FR::Target as Function>::Manager<'id>,
98        functions: impl IntoIterator<Item = FR>,
99    ) -> Self
100    where
101        FR::Target: Function,
102        INodeOfFunc<'id, FR::Target>: HasLevel,
103        TermOfFunc<'id, FR::Target>: AsciiDisplay,
104    {
105        self.add_diagram_start(FR::Target::REPR_ID, diagram_name);
106        crate::dddmp::ExportSettings::default()
107            .strict(false) // adjust names without error
108            .ascii()
109            .diagram_name(diagram_name)
110            .export(JsonStrWriter(&mut self.buf), manager, functions)
111            .unwrap(); // writing to a Vec<u8> should not lead to I/O errors
112        self.buf.extend_from_slice(b"\"}");
113
114        self
115    }
116
117    /// Add a decision diagram for visualization
118    ///
119    /// `diagram_name` can be used as an identifier in case you add multiple
120    /// diagrams. `functions` is an iterator over pairs of a [`Function`] and
121    /// a name.
122    ///
123    /// The visualization includes all nodes reachable from the root nodes
124    /// referenced by `functions`.
125    ///
126    /// Returns `self` to allow chaining.
127    ///
128    /// # Example
129    ///
130    /// ```
131    /// # use oxidd_core::function::Function;
132    /// # use oxidd_dump::Visualizer;
133    /// # fn vis<'id, F: Function>(manager: &F::Manager<'id>, phi: &F, res: &F)
134    /// # where
135    /// #    oxidd_core::function::INodeOfFunc<'id, F>: oxidd_core::HasLevel,
136    /// #    oxidd_core::function::TermOfFunc<'id, F>: oxidd_dump::AsciiDisplay,
137    /// # {
138    /// Visualizer::new()
139    ///     .add_with_names("my_diagram", manager, [(phi, "ϕ"), (res, "result")])
140    ///     .serve()
141    ///     .expect("failed to serve diagram due to I/O error")
142    /// # }
143    /// ```
144    pub fn add_with_names<'id, FR: std::ops::Deref, D: fmt::Display>(
145        mut self,
146        diagram_name: &str,
147        manager: &<FR::Target as Function>::Manager<'id>,
148        functions: impl IntoIterator<Item = (FR, D)>,
149    ) -> Self
150    where
151        FR::Target: Function,
152        INodeOfFunc<'id, FR::Target>: HasLevel,
153        TermOfFunc<'id, FR::Target>: AsciiDisplay,
154    {
155        self.add_diagram_start(FR::Target::REPR_ID, diagram_name);
156        crate::dddmp::ExportSettings::default()
157            .strict(false) // adjust names without error
158            .ascii()
159            .diagram_name(diagram_name)
160            .export_with_names(JsonStrWriter(&mut self.buf), manager, functions)
161            .unwrap(); // writing to a Vec<u8> should not lead to I/O errors
162        self.buf.extend_from_slice(b"\"}");
163
164        self
165    }
166
167    /// Remove all previously added decision diagrams
168    #[inline(always)]
169    pub fn clear(&mut self) {
170        self.buf.clear();
171    }
172
173    /// Serve the provided decision diagram for visualization
174    ///
175    /// Blocks until the visualization has been fetched by
176    /// [OxiDD-vis](https://oxidd.net/vis) (or another compatible tool).
177    ///
178    /// On success, all previously added decision diagrams are removed from the
179    /// internal buffer. On error, the internal buffer is left as-is.
180    pub fn serve(&mut self) -> io::Result<()> {
181        let port = self.port;
182        let listener = TcpListener::bind(("localhost", port))?;
183        println!("Data can be read on http://localhost:{port}");
184
185        let mut buf = EMPTY_RECV_BUF;
186        while !self.handle_client(listener.accept()?.0, &mut buf)? {}
187        Ok(())
188    }
189
190    /// Non-blocking version of [`Self::serve()`]
191    ///
192    /// Unlike [`Self::serve()`], this method sets the [`TcpListener`] into
193    /// [non-blocking mode][TcpListener::set_nonblocking()], allowing to run
194    /// different tasks while waiting for a connection by
195    /// [OxiDD-vis](https://oxidd.net/vis) (or another compatible tool).
196    ///
197    /// Note that you need to call [`poll()`][VisualizationListener::poll()]
198    /// repeatedly on the returned [`VisualizationListener`] to accept a TCP
199    /// connection.
200    pub fn serve_nonblocking(&mut self) -> io::Result<VisualizationListener<'_>> {
201        let port = self.port;
202        let listener = TcpListener::bind(("localhost", port))?;
203        listener.set_nonblocking(true)?;
204        println!("Data can be read on http://localhost:{port}");
205
206        Ok(VisualizationListener {
207            visualizer: self,
208            listener,
209            buf: Box::new(EMPTY_RECV_BUF),
210        })
211    }
212
213    /// Returns `Ok(true)` if the visualization has been sent, `Ok(false)` if
214    /// the client did not request `/diagrams`
215    fn handle_client(&mut self, stream: TcpStream, buf: &mut RecvBuf) -> io::Result<bool> {
216        use io::ErrorKind::*;
217        let mut stream = HttpStream::new(stream, buf)?;
218        loop {
219            return match self.handle_req(&mut stream) {
220                Ok(false) => continue,
221                Err(e) if matches!(e.kind(), UnexpectedEof | WouldBlock | TimedOut) => Ok(false),
222                res => res,
223            };
224        }
225    }
226
227    /// Returns `Ok(true)` if the visualization has been sent, `Ok(false)` if
228    /// request is for a path different from `/diagrams`
229    fn handle_req(&mut self, stream: &mut HttpStream) -> io::Result<bool> {
230        // Basic routing: only handle "GET /diagrams"
231        // After the path, there should be "HTTP/1.1" (or something alike),
232        // so expecting the space is fine.
233        if !stream.next_request_starts_with(b"GET /diagrams ")? {
234            stream.conn.write_all(
235                b"HTTP/1.1 404 NOT FOUND\r\n\
236                Content-Type: text/plain\r\n\
237                Content-Length: 9\r\n\
238                Access-Control-Allow-Origin: *\r\n\
239                \r\n\
240                Not Found",
241            )?;
242            return Ok(false);
243        }
244
245        let len = self.buf.len() + 2; // 2 -> account for outer brackets
246        write!(
247            stream.conn,
248            "HTTP/1.1 200 OK\r\n\
249            Content-Type: application/json\r\n\
250            Content-Length: {len}\r\n\
251            Access-Control-Allow-Origin: *\r\n\
252            \r\n\
253            ["
254        )?;
255        self.buf.push(b']');
256        if let Err(err) = stream.conn.write_all(&self.buf) {
257            self.buf.pop();
258            return Err(err);
259        }
260        self.buf.clear();
261        println!("Visualization has been sent!");
262        Ok(true)
263    }
264}
265
266type RecvBuf = [u8; 1024];
267const EMPTY_RECV_BUF: RecvBuf = [0; 1024];
268struct HttpStream<'a> {
269    recv_buf: &'a mut RecvBuf,
270    read_bytes: usize,
271    conn: TcpStream,
272}
273
274impl<'a> HttpStream<'a> {
275    fn new(stream: TcpStream, recv_buf: &'a mut RecvBuf) -> io::Result<Self> {
276        stream.set_nonblocking(false)?;
277        stream.set_read_timeout(Some(std::time::Duration::from_secs(3)))?;
278        Ok(Self {
279            recv_buf,
280            read_bytes: 0,
281            conn: stream,
282        })
283    }
284
285    /// Read the next HTTP request and check if it starts with the expected byte
286    /// sequence (e.g., `GET /foo `).
287    fn next_request_starts_with(&mut self, expected: &[u8]) -> io::Result<bool> {
288        debug_assert!(expected.len() <= self.recv_buf.len());
289        let result = loop {
290            // check before read, because we may already have received the
291            // request on the last call
292            if self.read_bytes >= expected.len() {
293                break self.recv_buf[..expected.len()] == *expected;
294            }
295            self.read_bytes += self.conn.read(&mut self.recv_buf[self.read_bytes..])?;
296            if self.read_bytes == 0 {
297                return Err(io::ErrorKind::UnexpectedEof.into());
298            }
299        };
300        // Find the message's end, which is "\r\n\r\n" according to the RFC.
301        // Instead, we just test for the newlines.
302        let mut last_nl = self.recv_buf.len(); // dummy value
303        loop {
304            for (i, &c) in self.recv_buf[..self.read_bytes].iter().enumerate() {
305                if c == b'\n' {
306                    if i == last_nl.wrapping_add(2) {
307                        // there may be another message in the buffer, copy it
308                        // to the beginning
309                        self.recv_buf.copy_within(i + 1..self.read_bytes, 0);
310                        self.read_bytes -= i + 1;
311                        return Ok(result);
312                    }
313                    last_nl = i;
314                }
315            }
316            last_nl = last_nl.wrapping_sub(self.read_bytes);
317            self.read_bytes = self.conn.read(self.recv_buf)?;
318            if self.read_bytes == 0 {
319                return Err(io::ErrorKind::UnexpectedEof.into());
320            }
321        }
322    }
323}
324
325/// A non-blocking [`TcpListener`] for decision diagram visualization
326pub struct VisualizationListener<'a> {
327    visualizer: &'a mut Visualizer,
328    listener: TcpListener,
329    buf: Box<RecvBuf>,
330}
331
332impl VisualizationListener<'_> {
333    /// Poll for clients like [OxiDD-vis](https://oxidd.net/vis)
334    ///
335    /// If a connection was established, this method will directly handle the
336    /// client. If the decision diagram(s) were successfully sent, the return
337    /// value is `Ok(true)`. If no client was available
338    /// ([`TcpListener::accept()`] returned an error with
339    /// [`io::ErrorKind::WouldBlock`]), the return value is `Ok(false)`.
340    /// `Err(..)` signals a communication error.
341    pub fn poll(&mut self) -> io::Result<bool> {
342        loop {
343            match self.listener.accept() {
344                Ok((stream, _)) => {
345                    if self.visualizer.handle_client(stream, &mut self.buf)? {
346                        return Ok(true);
347                    }
348                }
349                Err(e) => {
350                    return if e.kind() == io::ErrorKind::WouldBlock {
351                        Ok(false)
352                    } else {
353                        Err(e)
354                    };
355                }
356            }
357        }
358    }
359}
360
361#[inline]
362fn hex_digit(c: u8) -> u8 {
363    if c >= 10 {
364        b'a' + c - 10
365    } else {
366        b'0' + c
367    }
368}
369
370fn json_escape(target: &mut Vec<u8>, data: &[u8]) {
371    for &c in data {
372        let mut seq;
373        target.extend_from_slice(match c {
374            0x08 => b"\\b",
375            b'\t' => b"\\t",
376            b'\n' => b"\\n",
377            0x0c => b"\\f",
378            b'\r' => b"\\r",
379            b'\\' => b"\\\\",
380            b'"' => b"\\\"",
381            _ if c < 0x20 => {
382                seq = *b"\\u0000";
383                seq[4] = hex_digit(c >> 4);
384                seq[5] = hex_digit(c & 0b1111);
385                &seq
386            }
387            0x7f => b"\\u007f", // ASCII DEL
388            _ => {
389                target.push(c);
390                continue;
391            }
392        })
393    }
394}
395
396struct JsonStrWriter<'a>(&'a mut Vec<u8>);
397
398impl std::io::Write for JsonStrWriter<'_> {
399    #[inline]
400    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
401        json_escape(self.0, buf);
402        Ok(buf.len())
403    }
404
405    fn flush(&mut self) -> io::Result<()> {
406        Ok(())
407    }
408}