From 15a822d07bf6920f91d63281673c6f47a4e598f7 Mon Sep 17 00:00:00 2001 From: "Brian L. Troutwine" Date: Wed, 16 Oct 2024 14:47:27 -0700 Subject: [PATCH] split model into separate file Signed-off-by: Brian L. Troutwine --- lading/src/bin/logrotate_fs.rs | 307 ++----------------- lading/src/generator/file_gen.rs | 1 + lading/src/generator/file_gen/model.rs | 394 +++++++++++++++++++++++++ lading_payload/src/block.rs | 12 + 4 files changed, 439 insertions(+), 275 deletions(-) create mode 100644 lading/src/generator/file_gen/model.rs diff --git a/lading/src/bin/logrotate_fs.rs b/lading/src/bin/logrotate_fs.rs index 1c814ce58..045f1a360 100644 --- a/lading/src/bin/logrotate_fs.rs +++ b/lading/src/bin/logrotate_fs.rs @@ -3,9 +3,8 @@ use clap::Parser; use fuser::{ FileAttr, Filesystem, MountOption, ReplyAttr, ReplyData, ReplyDirectory, ReplyEntry, Request, }; +use lading::generator::file_gen::model; use lading_payload::block; -use lading_throttle::Throttle; -use model::NodeType; use rand::{rngs::SmallRng, SeedableRng}; use tracing::{error, info}; use tracing_subscriber::{fmt::format::FmtSpan, util::SubscriberInitExt}; @@ -78,261 +77,23 @@ pub enum Error { SerdeYaml(#[from] serde_yaml::Error), } -mod model { - use lading_payload::block; - use lading_throttle::Throttle; - - pub type Inode = usize; - pub type Tick = u64; - - #[derive(Debug)] - pub struct File<'a> { - name: String, - accumulated_bytes: u64, - last_tick: Tick, - content: &'a [u8], - parent: Inode, - } - - #[derive(Debug)] - pub struct Directory { - name: String, - children: &'static [Inode], - parent: Option, - } - - #[derive(Debug)] - pub enum Node<'a> { - File(File<'a>), - Directory(Directory), - } - - #[derive(Debug)] - pub struct State<'a> { - nodes: Vec>, - pub(super) root_inode: Inode, - throttle: Throttle, - block_cache: block::Cache, - } - - #[derive(Debug)] - pub struct NodeAttributes { - pub(super) inode: Inode, - pub(super) kind: NodeType, - pub(super) size: u64, - } - - #[derive(Debug)] - pub enum NodeType { - File, - Directory, - } - - impl<'a> State<'a> { - #[tracing::instrument(skip(content))] - pub fn new(bytes_per_tick: u64, content: &'a [u8]) -> State<'a> { - let root_inode: Inode = 0; // `/` - let logs_inode: Inode = 1; // `/logs` - let foo_log_inode: Inode = 2; // `/logs/foo.log` - - let mut nodes = Vec::new(); - - let root_dir = Directory { - name: "/".to_string(), - children: &[], // Will update later - parent: None, - }; - nodes.push(Node::Directory(root_dir)); - - let logs_dir = Directory { - name: "logs".to_string(), - children: &[], // Will update later - parent: Some(root_inode), - }; - nodes.push(Node::Directory(logs_dir)); - - let foo_log = File { - name: "foo.log".to_string(), - accumulated_bytes: 0, - last_tick: 0, - content, - parent: logs_inode, - }; - nodes.push(Node::File(foo_log)); - - // Update children - let logs_children: &'static [Inode] = Box::leak(vec![foo_log_inode].into_boxed_slice()); - - if let Node::Directory(ref mut dir) = nodes[logs_inode] { - dir.children = logs_children; - } - - let root_children: &'static [Inode] = Box::leak(vec![logs_inode].into_boxed_slice()); - - if let Node::Directory(ref mut dir) = nodes[root_inode] { - dir.children = root_children; - } - - State { - nodes, - root_inode, - bytes_per_tick, - } - } - - #[tracing::instrument(skip(self))] - pub fn lookup(&self, parent_inode: Inode, name: &str) -> Option { - if let Some(Node::Directory(dir)) = self.nodes.get(parent_inode) { - for &child_inode in dir.children { - let child_node = &self.nodes[child_inode]; - let child_name = match child_node { - Node::Directory(child_dir) => &child_dir.name, - Node::File(child_file) => &child_file.name, - }; - if child_name == name { - return Some(child_inode); - } - } - } - None - } - - #[tracing::instrument(skip(self))] - pub fn getattr(&self, inode: Inode) -> Option { - self.nodes.get(inode).map(|node| match node { - Node::File(file) => NodeAttributes { - inode, - kind: NodeType::File, - size: file.accumulated_bytes, - }, - Node::Directory(_) => NodeAttributes { - inode, - kind: NodeType::Directory, - size: 0, - }, - }) - } - - // TODO honestly i'm not sure how I want to handle this. I need to think - // about how the block mechanism works. - - // #[tracing::instrument(skip(self))] - // // TODO modify to use a Block so that we can simulate waiting for data. Also unclear if the block size mechanism is suitable given the way reads happen but h - // pub fn read( - // &mut self, - // inode: Inode, - // offset: usize, - // size: usize, - // tick: Tick, - // ) -> Option<&[u8]> { - // if let Some(Node::File(file)) = self.nodes.get_mut(inode) { - // // Update accumulated bytes as per the current tick - // let elapsed_ticks = tick.saturating_sub(file.last_tick); - // if elapsed_ticks > 0 { - // let additional_bytes = self.bytes_per_tick * elapsed_ticks; - // let new_accumulated = file.accumulated_bytes + additional_bytes; - - // // Update accumulated bytes and last_tick - // file.accumulated_bytes = new_accumulated; - // file.last_tick = tick; - // } - - // // Simulate infinite data by repeating the content - // let available_bytes = file.accumulated_bytes; - // if offset as u64 >= available_bytes { - // // No data available at this offset - // return Some(&[]); - // } - - // // Calculate how many bytes we can return - // let bytes_to_read = - // std::cmp::min(size as u64, available_bytes - offset as u64) as usize; - - // // Calculate the position in the content buffer - // let content_len = file.content.len(); - // let start = offset % content_len; - // let end = start + bytes_to_read.min(content_len - start); - - // Some(&file.content[start..end]) - // } else { - // None - // } - // } - - #[tracing::instrument(skip(self))] - pub fn readdir(&self, inode: Inode) -> Option<&[Inode]> { - if let Some(Node::Directory(dir)) = self.nodes.get(inode) { - Some(dir.children) - } else { - None - } - } - - #[tracing::instrument(skip(self))] - pub fn get_name(&self, inode: Inode) -> &str { - match &self.nodes[inode] { - Node::Directory(dir) => &dir.name, - Node::File(file) => &file.name, - } - } - - #[tracing::instrument(skip(self))] - pub fn get_file_type(&self, inode: Inode) -> fuser::FileType { - match &self.nodes[inode] { - Node::Directory(_) => fuser::FileType::Directory, - Node::File(_) => fuser::FileType::RegularFile, - } - } - - #[tracing::instrument(skip(self))] - pub fn get_parent_inode(&self, inode: Inode) -> Inode { - if inode == self.root_inode { - self.root_inode - } else { - match &self.nodes[inode] { - Node::Directory(dir) => dir.parent.unwrap_or(self.root_inode), - Node::File(file) => file.parent, - } - } - } - - #[tracing::instrument(skip(self))] - pub fn update_state(&mut self, current_tick: Tick) { - for node in self.nodes.iter_mut() { - if let Node::File(file) = node { - let elapsed_ticks = current_tick.saturating_sub(file.last_tick); - - if elapsed_ticks > 0 { - let additional_bytes = self.bytes_per_tick * elapsed_ticks; - let new_accumulated = file.accumulated_bytes + additional_bytes; - - // Update accumulated bytes and last_tick - file.accumulated_bytes = new_accumulated; - file.last_tick = current_tick; - } - } - } - } - } -} - const TTL: Duration = Duration::from_secs(1); // Attribute cache timeout #[derive(Debug)] -struct LogrotateFS<'a> { - state: model::State<'a>, +struct LogrotateFS { + state: model::State, start_time: std::time::Instant, } -impl<'a> LogrotateFS<'a> { +impl LogrotateFS { #[tracing::instrument(skip(self))] fn get_current_tick(&self) -> model::Tick { self.start_time.elapsed().as_secs() } #[tracing::instrument(skip(self))] - fn getattr_helper(&mut self, inode: usize) -> Option { - self.state.getattr(inode).map(|attr| FileAttr { + fn getattr_helper(&mut self, tick: model::Tick, inode: usize) -> Option { + self.state.getattr(tick, inode).map(|attr| FileAttr { ino: attr.inode as u64, size: attr.size, blocks: (attr.size + 511) / 512, @@ -341,10 +102,10 @@ impl<'a> LogrotateFS<'a> { ctime: UNIX_EPOCH, crtime: UNIX_EPOCH, kind: match attr.kind { - NodeType::File => fuser::FileType::RegularFile, - NodeType::Directory => fuser::FileType::Directory, + model::NodeType::File => fuser::FileType::RegularFile, + model::NodeType::Directory => fuser::FileType::Directory, }, - perm: if matches!(attr.kind, NodeType::Directory) { + perm: if matches!(attr.kind, model::NodeType::Directory) { 0o755 } else { 0o644 @@ -359,7 +120,7 @@ impl<'a> LogrotateFS<'a> { } } -impl<'a> Filesystem for LogrotateFS<'a> { +impl Filesystem for LogrotateFS { #[tracing::instrument(skip(self, _req, _config))] fn init( &mut self, @@ -372,11 +133,10 @@ impl<'a> Filesystem for LogrotateFS<'a> { #[tracing::instrument(skip(self, _req, reply))] fn lookup(&mut self, _req: &Request, parent: u64, name: &OsStr, reply: ReplyEntry) { let tick = self.get_current_tick(); - self.state.update_state(tick); let name_str = name.to_str().unwrap_or(""); - if let Some(ino) = self.state.lookup(parent as usize, name_str) { - if let Some(attr) = self.getattr_helper(ino) { + if let Some(ino) = self.state.lookup(tick, parent as usize, name_str) { + if let Some(attr) = self.getattr_helper(tick, ino) { reply.entry(&TTL, &attr, 0); return; } @@ -387,9 +147,8 @@ impl<'a> Filesystem for LogrotateFS<'a> { #[tracing::instrument(skip(self, _req, reply))] fn getattr(&mut self, _req: &Request, ino: u64, reply: ReplyAttr) { let tick = self.get_current_tick(); - self.state.update_state(tick); - if let Some(attr) = self.getattr_helper(ino as usize) { + if let Some(attr) = self.getattr_helper(tick, ino as usize) { reply.attr(&TTL, &attr); } else { reply.error(ENOENT); @@ -409,13 +168,12 @@ impl<'a> Filesystem for LogrotateFS<'a> { reply: ReplyData, ) { let tick = self.get_current_tick(); - self.state.update_state(tick); if let Some(data) = self .state .read(ino as usize, offset as usize, size as usize, tick) { - reply.data(data); + reply.data(&data); } else { reply.error(ENOENT); } @@ -431,32 +189,41 @@ impl<'a> Filesystem for LogrotateFS<'a> { mut reply: ReplyDirectory, ) { let tick = self.get_current_tick(); - self.state.update_state(tick); + self.state.advance_time(tick); if offset != 0 { reply.ok(); return; } + let root_inode = self.state.root_inode(); + if let Some(entries) = self.state.readdir(ino as usize) { let mut index = 1; // TODO fix let _ = reply.add(ino, index, fuser::FileType::Directory, "."); index += 1; - let root_inode = self.state.root_inode; if ino != root_inode as u64 { - let parent_ino = self.state.get_parent_inode(ino as usize); + let parent_ino = self + .state + .get_parent_inode(ino as usize) + .expect("inode must have parent"); // TODO fix let _ = reply.add(parent_ino as u64, index, fuser::FileType::Directory, ".."); index += 1; } - for &child_ino in entries { - let name = self.state.get_name(child_ino); - let file_type = self.state.get_file_type(child_ino); - // TODO fix - let _ = reply.add(child_ino as u64, index, file_type, name); - index += 1; + for child_ino in entries { + if let Some(name) = self.state.get_name(*child_ino) { + let file_type = self + .state + .get_file_type(*child_ino) + .expect("inode must have file type"); + let _ = reply.add(*child_ino as u64, index, file_type, name); + index += 1; + } else { + error!("child inode has no name"); + } } reply.ok(); } else { @@ -498,18 +265,8 @@ fn main() -> Result<(), Error> { ) .expect("block construction"); // TODO make this an Error - let throttle_conf = lading_throttle::Config::Stable; - let throttle = Throttle::new_with_config( - throttle_conf, - NonZeroU32::new( - args.bytes_per_second.get_bytes() as u32, // TODO avoid chopping this down - ) - .expect("zero value"), // TODO make this an error , - ); - let state = model::State::new( args.bytes_per_second.get_bytes() as u64, // Adjust units accordingly - throttle, block_cache, ); diff --git a/lading/src/generator/file_gen.rs b/lading/src/generator/file_gen.rs index 236953230..9542f8046 100644 --- a/lading/src/generator/file_gen.rs +++ b/lading/src/generator/file_gen.rs @@ -13,6 +13,7 @@ //! pub mod logrotate; +pub mod model; pub mod traditional; use std::str; diff --git a/lading/src/generator/file_gen/model.rs b/lading/src/generator/file_gen/model.rs new file mode 100644 index 000000000..03adb8d33 --- /dev/null +++ b/lading/src/generator/file_gen/model.rs @@ -0,0 +1,394 @@ +//! Model the internal logic of a logrotate filesystem. + +//use lading_payload::block; + +use std::collections::{HashMap, HashSet}; + +use bytes::Bytes; +use lading_payload::block; + +/// Time representation of the model +pub type Tick = u64; +/// The identification node number +pub type Inode = usize; + +/// Model representation of a `File`. Does not actually contain any bytes but +/// stores sufficient metadata to determine access patterns over time. +#[derive(Debug, Clone, Copy)] +pub struct File { + /// The parent `Node` of this `File`. + parent: Inode, + + /// The number of bytes written over the lifetime of this + /// `File`. Monotonically increasing. + /// + /// Property: `bytes_written` >= `bytes_read`. + bytes_written: u64, + /// The number of bytes read over the lifetime of this + /// `File`. Monotonically increasing. + /// + /// Property: `bytes_written` >= `bytes_read`. + bytes_read: u64, + + /// The `Tick` on which the `File` was last accessed. Updated on reads, + /// opens for reading. + access_tick: Tick, + /// The `Tick` on which the `File` was last modified. Updated on writes, + /// truncations or opens for writing. + modified_tick: Tick, + /// The `Tick` on which the `File` last had its status updated. Updated + /// whenever `access_tick` or `modified_tick` are updated. + /// + /// Property: `status_tick` == `modified_tick` || `status_tick` == `access_tick` + status_tick: Tick, + + /// The number of bytes that accumulate in this `File` per tick. + bytes_per_tick: u64, +} + +impl File { + /// Returns the number of bytes available to be read at instance `now`. + /// + /// This function returns the number of bytes that have been "written" to + /// the `File` and are available to be read. For instance, `modified_tick` + /// may be in the past but sufficient bytes have accumulated in the file for + /// non-zero reads to remain possible. Bytes will not be noted as consumed + /// until the caller calls [`File::read`]. + /// + /// Call to this file will advance `bytes_written` if `now` > + /// `modified_tick`. + /// + /// Returns 0 if `bytes_written` == `bytes_read`. + /// + /// # Panics + /// + /// Function will panic if `bytes_written` < `bytes_read`. This indicates a + /// catastrophic programming error. + pub fn available_to_read(&mut self, now: Tick) -> u64 { + self.advance_time(now); + + assert!(self.bytes_written >= self.bytes_read); + self.bytes_read.saturating_sub(self.bytes_read) + } + + /// Register a read. + /// + /// This function is pair to [`File::available_to_read`]. It's possible that + /// while `available_to_read` to read may signal some value X as being the + /// total bytes available the pool of entropy or caller will not read up to + /// X. As such we have to register how much is actually read. That's what + /// this function does. + /// + /// Updates `access_tick` to `now` and adds `request` to `bytes_read`. Time + /// will be advanced, meaning `modified_tick` may update. + pub fn read(&mut self, request: u64, now: Tick) { + self.advance_time(now); + + self.bytes_read = self.bytes_read.saturating_add(request); + self.access_tick = now; + self.status_tick = now; + } + + /// Register a read-only open. + /// + /// This function updates `access_time` to `now`. Time is advanced which may + /// result in more bytes being available in-file. + pub fn ro_open(&mut self, now: Tick) { + self.advance_time(now); + + self.access_tick = now; + } + + /// Run the clock forward in the `File`. + /// + /// This function runs the clock forward to `now`, updating `modified_tick` + /// and `status_tick` as bytes are continuously "written" to the `File`. + /// + /// Will have no result if `now` <= `modified_tick`. + fn advance_time(&mut self, now: Tick) { + if now <= self.modified_tick { + return; + } + + let diff = now.saturating_sub(self.modified_tick); + let bytes_accum = diff.saturating_mul(self.bytes_per_tick); + + self.bytes_written = self.bytes_written.saturating_add(bytes_accum); + self.modified_tick = now; + self.status_tick = now; + } +} + +/// Model representation of a `Directory`. Contains children are `Directory` +/// instances or `File` instances. Root directory will not have a `parent`. +#[derive(Debug)] +pub struct Directory { + children: HashSet, + parent: Option, +} + +/// A filesystem object, either a `File` or a `Directory`. +#[derive(Debug)] +pub enum Node { + /// A [`File`] + File { + /// The name of this file. If the full path is /logs/foo.log then this is "foo.log". + name: String, + /// The `File` instance. + file: File, + }, + /// A [`Directory`] + Directory { + /// the name of this directory. If the full path is /logs then this is "logs". + name: String, + /// The `Directory` instance. + dir: Directory, + }, +} + +/// The state of the filesystem +/// +/// This structure is responsible for maintenance of the structure of the +/// filesystem. It does not contain any bytes, the caller must maintain this +/// themselves. +#[derive(Debug)] +pub struct State { + nodes: HashMap, + root_inode: Inode, + now: Tick, + block_cache: block::Cache, +} + +/// The attributes of a `Node`. +#[derive(Debug, Clone, Copy)] +pub struct NodeAttributes { + /// The id of the node. + pub inode: Inode, + /// The kind, whether a file or directory. + pub kind: NodeType, + /// The size in bytes. + pub size: u64, +} + +/// Describe whether the Node is a File or Directory. +#[derive(Debug, Clone, Copy)] +pub enum NodeType { + /// A [`File`] + File, + /// A [`Directory`] + Directory, +} + +impl State { + /// Create a new instance of `State`. + #[tracing::instrument] + pub fn new(bytes_per_tick: u64, block_cache: block::Cache) -> State { + let root_inode: Inode = 0; // `/` + let logs_inode: Inode = 1; // `/logs` + let foo_log_inode: Inode = 2; // `/logs/foo.log` + + let mut nodes = HashMap::new(); + + let root_dir = Directory { + children: HashSet::new(), + parent: None, + }; + nodes.insert( + root_inode, + Node::Directory { + name: "/".to_string(), + dir: root_dir, + }, + ); + + let logs_dir = Directory { + children: HashSet::new(), + parent: Some(root_inode), + }; + nodes.insert( + logs_inode, + Node::Directory { + name: "/logs".to_string(), + dir: logs_dir, + }, + ); + + let foo_log = File { + parent: logs_inode, + + bytes_written: 0, + bytes_read: 0, + + access_tick: 0, + modified_tick: 0, + status_tick: 0, + + bytes_per_tick, + }; + nodes.insert( + foo_log_inode, + Node::File { + name: "/logs/foo.log".to_string(), + file: foo_log, + }, + ); + + // NOTE this structure is going to be a problem when I include rotating + // files. Specifically the inodes will need to change so there might + // need to be a concept of a SuperFile that holds inodes or something + // for its rotating children? Dunno. An array with a current pointer? + + State { + nodes, + root_inode, + now: 0, + block_cache, + } + } + + /// Advance time in the model. + /// + /// # Panics + /// + /// Will panic if passed `now` is less than recorded `now`. Time can only + /// advance. + pub fn advance_time(&mut self, now: Tick) { + // nothing yet beyond updating the clock, rotations to come + assert!(now >= self.now); + self.now = now; + } + + /// Look up the Inode for a given `name`. + /// + /// This function searches under `parent_inode` for a match to `name`, + /// returning any inode that happens to match. Time will be advanced to + /// `now`. + #[tracing::instrument(skip(self))] + pub fn lookup(&mut self, now: Tick, parent_inode: Inode, name: &str) -> Option { + self.advance_time(now); + + if let Some(Node::Directory { name, dir }) = self.nodes.get(&parent_inode) { + for child_inode in &dir.children { + let child_node = &self + .nodes + .get(child_inode) + .expect("catastrophic programming error"); + let child_name = match child_node { + Node::Directory { name, .. } | Node::File { name, .. } => name, + }; + if child_name == name { + return Some(*child_inode); + } + } + } + None + } + + /// Look up the attributes for an `Inode`. + /// + /// Time will be advanced to `now`. + #[tracing::instrument(skip(self))] + pub fn getattr(&mut self, now: Tick, inode: Inode) -> Option { + self.advance_time(now); + + self.nodes.get(&inode).map(|node| match node { + Node::File { file, .. } => NodeAttributes { + inode, + kind: NodeType::File, + size: file.bytes_written, + }, + Node::Directory { .. } => NodeAttributes { + inode, + kind: NodeType::Directory, + size: 0, + }, + }) + } + + /// Read `size` bytes from `inode`. + /// + /// We do not model a position in files, meaning that `offset` is + /// ignored. An attempt will be made to read `size` bytes at time `tick` -- + /// time will be advanced -- and a slice up to `size` bytes will be returned + /// or `None` if no bytes are available to be read. + #[tracing::instrument(skip(self))] + pub fn read(&mut self, inode: Inode, offset: usize, size: usize, now: Tick) -> Option { + self.advance_time(now); + + match self.nodes.get_mut(&inode) { + Some(Node::File { + name: _, + ref mut file, + }) => { + let available = file.available_to_read(now); + if available == 0 { + return None; + } + + let block_len = self.block_cache.peek_next().total_bytes.get() as usize; + if block_len <= size { + let block = self.block_cache.next_block(); + file.read(block_len as u64, now); + Some(block.bytes.clone()) + } else { + None + } + } + Some(Node::Directory { .. }) | None => None, + } + } + + /// Read inodes from a directory + /// + /// Returns None if the inode is a `File`, else returns the hashset of + /// children inodes. + /// + /// Function does not advance time in the model. + #[tracing::instrument(skip(self))] + pub fn readdir(&self, inode: Inode) -> Option<&HashSet> { + if let Some(Node::Directory { dir, .. }) = self.nodes.get(&inode) { + Some(&dir.children) + } else { + None + } + } + + /// Get the name of an inode if it exists + #[tracing::instrument(skip(self))] + pub fn get_name(&self, inode: Inode) -> Option<&str> { + self.nodes + .get(&inode) + .map(|node| match node { + Node::Directory { name, .. } | Node::File { name, .. } => name, + }) + .map(String::as_str) + } + + /// Get the fuser file type of an inode if it exists + #[tracing::instrument(skip(self))] + pub fn get_file_type(&self, inode: Inode) -> Option { + self.nodes.get(&inode).map(|node| match node { + Node::Directory { .. } => fuser::FileType::Directory, + Node::File { .. } => fuser::FileType::RegularFile, + }) + } + + /// Return the parent inode of an inode, if it exists + #[tracing::instrument(skip(self))] + pub fn get_parent_inode(&self, inode: Inode) -> Option { + if inode == self.root_inode { + Some(self.root_inode) + } else { + self.nodes.get(&inode).map(|node| match node { + Node::Directory { dir, .. } => dir.parent.unwrap_or(self.root_inode), + Node::File { file, .. } => file.parent, + }) + } + } + + /// Return the root inode of this state + #[must_use] + pub fn root_inode(&self) -> Inode { + self.root_inode + } +} diff --git a/lading_payload/src/block.rs b/lading_payload/src/block.rs index 59605d519..5a928905a 100644 --- a/lading_payload/src/block.rs +++ b/lading_payload/src/block.rs @@ -343,6 +343,18 @@ impl Cache { } } + /// Peek at the next `Block` from the `Cache`. + /// + /// This is a block function that returns a reference to the next `Block` + /// instance although the cache is not advanced by this call. Callers must + /// call [`Self::next_block`] or this cache will not advance. + #[must_use] + pub fn peek_next(&self) -> &Block { + match self { + Self::Fixed { idx, blocks } => &blocks[*idx], + } + } + /// Return a `Block` from the `Cache` /// /// This is a blocking function that returns a single `Block` instance as