Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2025-01-18 10:10:46

0001 /// \file ROOT/RNTupleInspector.hxx
0002 /// \ingroup NTuple ROOT7
0003 /// \author Florine de Geus <florine.de.geus@cern.ch>
0004 /// \date 2023-01-09
0005 /// \warning This is part of the ROOT 7 prototype! It will change without notice. It might trigger earthquakes. Feedback
0006 /// is welcome!
0007 
0008 /*************************************************************************
0009  * Copyright (C) 1995-2023, Rene Brun and Fons Rademakers.               *
0010  * All rights reserved.                                                  *
0011  *                                                                       *
0012  * For the licensing terms see $ROOTSYS/LICENSE.                         *
0013  * For the list of contributors see $ROOTSYS/README/CREDITS.             *
0014  *************************************************************************/
0015 
0016 #ifndef ROOT7_RNTupleInspector
0017 #define ROOT7_RNTupleInspector
0018 
0019 #include <ROOT/RError.hxx>
0020 #include <ROOT/RNTupleDescriptor.hxx>
0021 
0022 #include <TFile.h>
0023 #include <TH1D.h>
0024 #include <THStack.h>
0025 
0026 #include <cstdlib>
0027 #include <iostream>
0028 #include <memory>
0029 #include <numeric>
0030 #include <regex>
0031 #include <vector>
0032 
0033 namespace ROOT {
0034 namespace Experimental {
0035 
0036 class RNTuple;
0037 
0038 namespace Internal {
0039 class RPageSource;
0040 } // namespace Internal
0041 
0042 enum class ENTupleInspectorPrintFormat { kTable, kCSV };
0043 enum class ENTupleInspectorHist { kCount, kNElems, kCompressedSize, kUncompressedSize };
0044 
0045 // clang-format off
0046 /**
0047 \class ROOT::Experimental::RNTupleInspector
0048 \ingroup NTuple
0049 \brief Inspect on-disk and storage-related information of an RNTuple.
0050 
0051 The RNTupleInspector can be used for studying an RNTuple in terms of its storage efficiency. It provides information on
0052 the level of the RNTuple itself, on the (sub)field level and on the column level.
0053 
0054 Example usage:
0055 
0056 ~~~ {.cpp}
0057 #include <ROOT/RNTuple.hxx>
0058 #include <ROOT/RNTupleInspector.hxx>
0059 
0060 #include <iostream>
0061 
0062 using ROOT::Experimental::RNTuple;
0063 using ROOT::Experimental::RNTupleInspector;
0064 
0065 auto file = TFile::Open("data.rntuple");
0066 auto rntuple = file->Get<RNTuple>("NTupleName");
0067 auto inspector = RNTupleInspector::Create(rntuple).Unwrap();
0068 
0069 std::cout << "The compression factor is " << inspector->GetCompressionFactor()
0070           << " using compression settings " << inspector->GetCompressionSettings()
0071           << std::endl;
0072 ~~~
0073 */
0074 // clang-format on
0075 class RNTupleInspector {
0076 public:
0077    /////////////////////////////////////////////////////////////////////////////
0078    /// \brief Provides column-level storage information.
0079    ///
0080    /// The RColumnInspector class provides storage information for an individual column. This information is partly
0081    /// collected during the construction of the RNTupleInspector object, and can partly be accessed using the
0082    /// RColumnInspector that belongs to this field.
0083    class RColumnInspector {
0084    private:
0085       const RColumnDescriptor &fColumnDescriptor;
0086       const std::vector<std::uint64_t> fCompressedPageSizes = {};
0087       std::uint32_t fElementSize = 0;
0088       std::uint64_t fNElements = 0;
0089 
0090    public:
0091       RColumnInspector(const RColumnDescriptor &colDesc, const std::vector<std::uint64_t> &compressedPageSizes,
0092                        std::uint32_t elemSize, std::uint64_t nElems)
0093          : fColumnDescriptor(colDesc),
0094            fCompressedPageSizes(compressedPageSizes),
0095            fElementSize(elemSize),
0096            fNElements(nElems){};
0097       ~RColumnInspector() = default;
0098 
0099       const RColumnDescriptor &GetDescriptor() const { return fColumnDescriptor; }
0100       const std::vector<std::uint64_t> &GetCompressedPageSizes() const { return fCompressedPageSizes; }
0101       std::uint64_t GetNPages() const { return fCompressedPageSizes.size(); }
0102       std::uint64_t GetCompressedSize() const
0103       {
0104          return std::accumulate(fCompressedPageSizes.begin(), fCompressedPageSizes.end(), 0);
0105       }
0106       std::uint64_t GetUncompressedSize() const { return fElementSize * fNElements; }
0107       std::uint64_t GetElementSize() const { return fElementSize; }
0108       std::uint64_t GetNElements() const { return fNElements; }
0109       EColumnType GetType() const { return fColumnDescriptor.GetModel().GetType(); }
0110    };
0111 
0112    /////////////////////////////////////////////////////////////////////////////
0113    /// \brief Provides field-level storage information.
0114    ///
0115    /// The RFieldTreeInspector class provides storage information for a field **and** its subfields. This information is
0116    /// partly collected during the construction of the RNTupleInspector object, and can partly be accessed using
0117    /// the RFieldDescriptor that belongs to this field.
0118    class RFieldTreeInspector {
0119    private:
0120       const RFieldDescriptor &fRootFieldDescriptor;
0121       std::uint64_t fCompressedSize = 0;
0122       std::uint64_t fUncompressedSize = 0;
0123 
0124    public:
0125       RFieldTreeInspector(const RFieldDescriptor &fieldDesc, std::uint64_t onDiskSize, std::uint64_t inMemSize)
0126          : fRootFieldDescriptor(fieldDesc), fCompressedSize(onDiskSize), fUncompressedSize(inMemSize){};
0127       ~RFieldTreeInspector() = default;
0128 
0129       const RFieldDescriptor &GetDescriptor() const { return fRootFieldDescriptor; }
0130       std::uint64_t GetCompressedSize() const { return fCompressedSize; }
0131       std::uint64_t GetUncompressedSize() const { return fUncompressedSize; }
0132    };
0133 
0134 private:
0135    std::unique_ptr<Internal::RPageSource> fPageSource;
0136    std::unique_ptr<RNTupleDescriptor> fDescriptor;
0137    int fCompressionSettings = -1;
0138    std::uint64_t fCompressedSize = 0;
0139    std::uint64_t fUncompressedSize = 0;
0140 
0141    std::unordered_map<int, RColumnInspector> fColumnInfo;
0142    std::unordered_map<int, RFieldTreeInspector> fFieldTreeInfo;
0143 
0144    RNTupleInspector(std::unique_ptr<Internal::RPageSource> pageSource);
0145 
0146    /////////////////////////////////////////////////////////////////////////////
0147    /// \brief Gather column-level and RNTuple-level information.
0148    ///
0149    /// \note This method is called when the RNTupleInspector is initially created. This means that anything unexpected
0150    /// about the RNTuple itself (e.g. inconsistent compression settings across clusters) will be detected here.
0151    /// Therefore, any related exceptions will be thrown on creation of the inspector.
0152    void CollectColumnInfo();
0153 
0154    /////////////////////////////////////////////////////////////////////////////
0155    /// \brief Recursively gather field-level information.
0156    ///
0157    /// \param[in] fieldId The ID of the field from which to start the recursive traversal. Typically this is the "zero
0158    /// ID", i.e. the logical parent of all top-level fields.
0159    ///
0160    /// \return The RFieldTreeInspector for the provided field ID.
0161    ///
0162    /// This method is called when the RNTupleInspector is initially created.
0163    RFieldTreeInspector CollectFieldTreeInfo(DescriptorId_t fieldId);
0164 
0165    /////////////////////////////////////////////////////////////////////////////
0166    /// \brief Get the columns that make up the given field, including its subfields.
0167    ///
0168    /// \param [in] fieldId The ID of the field for which to collect the columns.
0169    ///
0170    /// \return A vector containing the IDs of all columns for the provided field ID.
0171    std::vector<DescriptorId_t> GetColumnsByFieldId(DescriptorId_t fieldId) const;
0172 
0173 public:
0174    RNTupleInspector(const RNTupleInspector &other) = delete;
0175    RNTupleInspector &operator=(const RNTupleInspector &other) = delete;
0176    RNTupleInspector(RNTupleInspector &&other) = delete;
0177    RNTupleInspector &operator=(RNTupleInspector &&other) = delete;
0178    ~RNTupleInspector() = default;
0179 
0180    /////////////////////////////////////////////////////////////////////////////
0181    /// \brief Create a new RNTupleInspector.
0182    ///
0183    /// \param[in] sourceNTuple A pointer to the RNTuple to be inspected.
0184    ///
0185    /// \return A pointer to the newly created RNTupleInspector.
0186    ///
0187    /// \note When this factory method is called, all required static information is collected from the RNTuple's fields
0188    /// and underlying columns are collected at ones. This means that when any inconsistencies are encountered (e.g.
0189    /// inconsistent compression across clusters), it will throw an error here.
0190    static std::unique_ptr<RNTupleInspector> Create(RNTuple *sourceNTuple);
0191 
0192    /////////////////////////////////////////////////////////////////////////////
0193    /// \brief Create a new RNTupleInspector.
0194    ///
0195    /// \param[in] ntupleName The name of the RNTuple to be inspected.
0196    /// \param[in] storage The path or URI to the RNTuple to be inspected.
0197    ///
0198    /// \see Create(RNTuple *sourceNTuple)
0199    static std::unique_ptr<RNTupleInspector> Create(std::string_view ntupleName, std::string_view storage);
0200 
0201    /////////////////////////////////////////////////////////////////////////////
0202    /// \brief Get the descriptor for the RNTuple being inspected.
0203    ///
0204    /// \return A static copy of the RNTupleDescriptor belonging to the inspected RNTuple.
0205    RNTupleDescriptor *GetDescriptor() const { return fDescriptor.get(); }
0206 
0207    /////////////////////////////////////////////////////////////////////////////
0208    /// \brief Get the compression settings of the RNTuple being inspected.
0209    ///
0210    /// \return The integer representation (\f$algorithm * 10 + level\f$, where \f$algorithm\f$ follows
0211    /// ROOT::RCompressionSetting::ELevel::EValues) of the compression settings used for the inspected RNTuple.
0212    ///
0213    /// \note Here, we assume that the compression settings are consistent across all clusters and columns. If this is
0214    /// not the case, an exception will be thrown when RNTupleInspector::Create is called.
0215    int GetCompressionSettings() const { return fCompressionSettings; }
0216 
0217    /////////////////////////////////////////////////////////////////////////////
0218    /// \brief Get a string describing compression settings of the RNTuple being inspected.
0219    ///
0220    /// \return A string describing the compression used for the inspected RNTuple. The format of the string is
0221    /// `"A (level L)"`, where `A` is the name of the compression algorithm and `L` the compression level.
0222    ///
0223    /// \note Here, we assume that the compression settings are consistent across all clusters and columns. If this is
0224    /// not the case, an exception will be thrown when RNTupleInspector::Create is called.
0225    std::string GetCompressionSettingsAsString() const;
0226 
0227    /////////////////////////////////////////////////////////////////////////////
0228    /// \brief Get the compressed, on-disk size of the RNTuple being inspected.
0229    ///
0230    /// \return The compressed size of the inspected RNTuple, in bytes, excluding the size of the header and footer.
0231    std::uint64_t GetCompressedSize() const { return fCompressedSize; }
0232 
0233    /////////////////////////////////////////////////////////////////////////////
0234    /// \brief Get the uncompressed total size of the RNTuple being inspected.
0235    ///
0236    /// \return The uncompressed size of the inspected RNTuple, in bytes, excluding the size of the header and footer.
0237    std::uint64_t GetUncompressedSize() const { return fUncompressedSize; }
0238 
0239    /////////////////////////////////////////////////////////////////////////////
0240    /// \brief Get the compression factor of the RNTuple being inspected.
0241    ///
0242    /// \return The compression factor of the inspected RNTuple.
0243    ///
0244    /// The compression factor shows how well the data present in the RNTuple is compressed by the compression settings
0245    /// that were used. The compression factor is calculated as \f$size_{uncompressed} / size_{compressed}\f$.
0246    float GetCompressionFactor() const { return (float)fUncompressedSize / (float)fCompressedSize; }
0247 
0248    /////////////////////////////////////////////////////////////////////////////
0249    /// \brief Get storage information for a given column.
0250    ///
0251    /// \param[in] physicalColumnId The physical ID of the column for which to get the information.
0252    ///
0253    /// \return The storage information for the provided column.
0254    const RColumnInspector &GetColumnInspector(DescriptorId_t physicalColumnId) const;
0255 
0256    /////////////////////////////////////////////////////////////////////////////
0257    /// \brief Get the number of columns of a given type present in the RNTuple.
0258    ///
0259    /// \param[in] colType The column type to count, as defined by ROOT::Experimental::EColumnType.
0260    ///
0261    /// \return The number of columns present in the inspected RNTuple of the provided type.
0262    size_t GetColumnCountByType(EColumnType colType) const;
0263 
0264    /////////////////////////////////////////////////////////////////////////////
0265    /// \brief Get the IDs of all columns with the given type.
0266    ///
0267    /// \param[in] colType The column type to collect, as defined by ROOT::Experimental::EColumnType.
0268    ///
0269    /// \return A vector containing the physical IDs of columns of the provided type.
0270    const std::vector<DescriptorId_t> GetColumnsByType(EColumnType colType);
0271 
0272    /////////////////////////////////////////////////////////////////////////////
0273    /// \brief Get all column types present in the RNTuple being inspected.
0274    ///
0275    /// \return A vector containing all column types present in the RNTuple.
0276    const std::vector<EColumnType> GetColumnTypes();
0277 
0278    /////////////////////////////////////////////////////////////////////////////
0279    /// \brief Print storage information per column type.
0280    ///
0281    /// \param[in] format Whether to print the information as a (markdown-parseable) table or in CSV format.
0282    /// \param[in] output Where to write the output to. Default is `stdout`.
0283    ///
0284    /// The output includes for each column type its count, the total number of elements, the compressed size and the
0285    /// uncompressed size.
0286    ///
0287    /// **Example: printing the column type information of an RNTuple as a table**
0288    /// ~~~ {.cpp}
0289    /// #include <ROOT/RNTupleInspector.hxx>
0290    /// using ROOT::Experimental::RNTupleInspector;
0291    /// using ROOT::Experimental::ENTupleInspectorPrintFormat;
0292    ///
0293    /// auto inspector = RNTupleInspector::Create("myNTuple", "some/file.root");
0294    /// inspector->PrintColumnTypeInfo();
0295    /// ~~~
0296    /// Output:
0297    /// ~~~
0298    ///  column type    | count   | # elements      | compressed bytes  | uncompressed bytes
0299    /// ----------------|---------|-----------------|-------------------|--------------------
0300    ///    SplitIndex64 |       2 |             150 |                72 |               1200
0301    ///     SplitReal32 |       4 |             300 |               189 |               1200
0302    ///     SplitUInt32 |       3 |             225 |               123 |                900
0303    /// ~~~
0304    ///
0305    /// **Example: printing the column type information of an RNTuple in CSV format**
0306    /// ~~~ {.cpp}
0307    /// #include <ROOT/RNTupleInspector.hxx>
0308    /// using ROOT::Experimental::RNTupleInspector;
0309    /// using ROOT::Experimental::ENTupleInspectorPrintFormat;
0310    ///
0311    /// auto inspector = RNTupleInspector::Create("myNTuple", "some/file.root");
0312    /// inspector->PrintColumnTypeInfo();
0313    /// ~~~
0314    /// Output:
0315    /// ~~~
0316    /// columnType,count,nElements,compressedSize,uncompressedSize
0317    /// SplitIndex64,2,150,72,1200
0318    /// SplitReal32,4,300,189,1200
0319    /// SplitUInt32,3,225,123,900
0320    /// ~~~
0321    void PrintColumnTypeInfo(ENTupleInspectorPrintFormat format = ENTupleInspectorPrintFormat::kTable,
0322                             std::ostream &output = std::cout);
0323 
0324    /////////////////////////////////////////////////////////////////////////////
0325    /// \brief Get a histogram showing information for each column type present,
0326    ///
0327    /// \param[in] histKind Which type of information should be returned.
0328    /// \param[in] histName The name of the histogram. An empty string means a default name will be used.
0329    /// \param[in] histTitle The title of the histogram. An empty string means a default title will be used.
0330    ///
0331    /// \return A pointer to a `TH1D` containing the specified kind of information.
0332    ///
0333    /// Get a histogram showing the count, number of elements, size on disk, or size in memory for each column
0334    /// type present in the inspected RNTuple.
0335    std::unique_ptr<TH1D> GetColumnTypeInfoAsHist(ENTupleInspectorHist histKind, std::string_view histName = "",
0336                                                  std::string_view histTitle = "");
0337 
0338    /////////////////////////////////////////////////////////////////////////////
0339    /// \brief Get a histogram containing the size distribution of the compressed pages for an individual column.
0340    ///
0341    /// \param[in] physicalColumnId The physical ID of the column for which to get the page size distribution.
0342    /// \param[in] histName The name of the histogram. An empty string means a default name will be used.
0343    /// \param[in] histTitle The title of the histogram. An empty string means a default title will be used.
0344    /// \param[in] nBins The desired number of histogram bins.
0345    ///
0346    /// \return A pointer to a `TH1D` containing the page size distribution.
0347    ///
0348    /// The x-axis will range from the smallest page size, to the largest (inclusive).
0349    std::unique_ptr<TH1D> GetPageSizeDistribution(DescriptorId_t physicalColumnId, std::string histName = "",
0350                                                  std::string histTitle = "", size_t nBins = 64);
0351 
0352    /////////////////////////////////////////////////////////////////////////////
0353    /// \brief Get a histogram containing the size distribution of the compressed pages for all columns of a given type.
0354    ///
0355    /// \param[in] colType The column type for which to get the size distribution, as defined by
0356    /// ROOT::Experimental::EColumnType.
0357    /// \param[in] histName The name of the histogram. An empty string means a default name will be used.
0358    /// \param[in] histTitle The title of the histogram. An empty string means a default title will be used.
0359    /// \param[in] nBins The desired number of histogram bins.
0360    ///
0361    /// \return A pointer to a `TH1D` containing the page size distribution.
0362    ///
0363    /// The x-axis will range from the smallest page size, to the largest (inclusive).
0364    std::unique_ptr<TH1D> GetPageSizeDistribution(EColumnType colType, std::string histName = "",
0365                                                  std::string histTitle = "", size_t nBins = 64);
0366 
0367    /////////////////////////////////////////////////////////////////////////////
0368    /// \brief Get a histogram containing the size distribution of the compressed pages for a collection columns.
0369    ///
0370    /// \param[in] colIds The physical IDs of the columns for which to get the page size distribution.
0371    /// \param[in] histName The name of the histogram. An empty string means a default name will be used.
0372    /// \param[in] histTitle The title of the histogram. An empty string means a default title will be used.
0373    /// \param[in] nBins The desired number of histogram bins.
0374    ///
0375    /// \return A pointer to a `TH1D` containing the (cumulative) page size distribution.
0376    ///
0377    /// The x-axis will range from the smallest page size, to the largest (inclusive).
0378    std::unique_ptr<TH1D> GetPageSizeDistribution(std::initializer_list<DescriptorId_t> colIds,
0379                                                  std::string histName = "", std::string histTitle = "",
0380                                                  size_t nBins = 64);
0381 
0382    /////////////////////////////////////////////////////////////////////////////
0383    /// \brief Get a histogram containing the size distribution of the compressed pages for all columns of a given list
0384    /// of types.
0385    ///
0386    /// \param[in] colTypes The column types for which to get the size distribution, as defined by
0387    /// ROOT::Experimental::EColumnType. The default is an empty vector, which indicates that the distribution for *all*
0388    /// physical columns will be returned.
0389    /// \param[in] histName The name of the histogram. An empty string means a default name will be used. The name of
0390    /// each histogram inside the `THStack` will be `histName + colType`.
0391    /// \param[in] histTitle The title of the histogram. An empty string means a default title will be used.
0392    /// \param[in] nBins The desired number of histogram bins.
0393    ///
0394    /// \return A pointer to a `THStack` with one histogram for each column type.
0395    ///
0396    /// The x-axis will range from the smallest page size, to the largest (inclusive).
0397    ///
0398    /// **Example: Drawing a non-stacked page size distribution with a legend**
0399    /// ~~~ {.cpp}
0400    /// auto canvas = std::make_unique<TCanvas>();
0401    /// auto inspector = RNTupleInspector::Create("myNTuple", "ntuple.root");
0402    ///
0403    /// // We want to show the page size distributions of columns with type `kSplitReal32` and `kSplitReal64`.
0404    /// auto hist = inspector->GetPageSizeDistribution(
0405    ///     {ROOT::Experimental::EColumnType::kSplitReal32,
0406    ///      ROOT::Experimental::EColumnType::kSplitReal64});
0407    /// // The "PLC" option automatically sets the line color for each histogram in the `THStack`.
0408    /// // The "NOSTACK" option will draw the histograms on top of each other instead of stacked.
0409    /// hist->DrawClone("PLC NOSTACK");
0410    /// canvas->BuildLegend(0.7, 0.8, 0.89, 0.89);
0411    /// canvas->DrawClone();
0412    /// ~~~
0413    std::unique_ptr<THStack> GetPageSizeDistribution(std::initializer_list<EColumnType> colTypes = {},
0414                                                     std::string histName = "", std::string histTitle = "",
0415                                                     size_t nBins = 64);
0416 
0417    /////////////////////////////////////////////////////////////////////////////
0418    /// \brief Get storage information for a given (sub)field by ID.
0419    ///
0420    /// \param[in] fieldId The ID of the (sub)field for which to get the information.
0421    ///
0422    /// \return The storage information inspector for the provided (sub)field tree.
0423    const RFieldTreeInspector &GetFieldTreeInspector(DescriptorId_t fieldId) const;
0424 
0425    /////////////////////////////////////////////////////////////////////////////
0426    /// \brief Get a storage information inspector for a given (sub)field by name, including its subfields.
0427    ///
0428    /// \param[in] fieldName The name of the (sub)field for which to get the information.
0429    ///
0430    /// \return The storage information inspector for the provided (sub)field tree.
0431    const RFieldTreeInspector &GetFieldTreeInspector(std::string_view fieldName) const;
0432 
0433    /////////////////////////////////////////////////////////////////////////////
0434    /// \brief Get the number of fields of a given type or class present in the RNTuple.
0435    ///
0436    /// \param[in] typeNamePattern The type or class name to count. May contain regular expression patterns for grouping
0437    /// multiple kinds of types or classes.
0438    /// \param[in] searchInSubFields If set to `false`, only top-level fields will be considered.
0439    ///
0440    /// \return The number of fields that matches the provided type.
0441    size_t GetFieldCountByType(const std::regex &typeNamePattern, bool searchInSubFields = true) const;
0442 
0443    /////////////////////////////////////////////////////////////////////////////
0444    /// \brief Get the number of fields of a given type or class present in the RNTuple.
0445    ///
0446    /// \see GetFieldCountByType(const std::regex &typeNamePattern, bool searchInSubFields) const
0447    size_t GetFieldCountByType(std::string_view typeNamePattern, bool searchInSubFields = true) const
0448    {
0449       return GetFieldCountByType(std::regex{std::string(typeNamePattern)}, searchInSubFields);
0450    }
0451 
0452    /////////////////////////////////////////////////////////////////////////////
0453    /// \brief Get the IDs of (sub-)fields whose name matches the given string.
0454    ///
0455    /// \param[in] fieldNamePattern The name of the field name to get. Because field names are unique by design,
0456    /// providing a single field name will return a vector containing just the ID of that field. However, regular
0457    /// expression patterns are supported in order to get the IDs of all fields whose name follow a certain structure.
0458    /// \param[in] searchInSubFields If set to `false`, only top-level fields will be considered.
0459    ///
0460    /// \return A vector containing the IDs of fields that match the provided name.
0461    const std::vector<DescriptorId_t>
0462    GetFieldsByName(const std::regex &fieldNamePattern, bool searchInSubFields = true) const;
0463 
0464    /////////////////////////////////////////////////////////////////////////////
0465    /// \brief Get the IDs of (sub-)fields whose name matches the given string.
0466    ///
0467    /// \see GetFieldsByName(const std::regex &fieldNamePattern, bool searchInSubFields) const
0468    const std::vector<DescriptorId_t> GetFieldsByName(std::string_view fieldNamePattern, bool searchInSubFields = true)
0469    {
0470       return GetFieldsByName(std::regex{std::string(fieldNamePattern)}, searchInSubFields);
0471    }
0472 };
0473 } // namespace Experimental
0474 } // namespace ROOT
0475 
0476 #endif // ROOT7_RNTupleInspector