Overview
This is a code review as part of the larger project of reviewing the various data formats packages, as described in https://github.com/DUNE-DAQ/daq-deliverables/issues/215. For details / motivations behind my suggestions, please click the link. In this Issue, I'll start by providing some general comments about the repository, then I'll go file-by-file.
General Comments
Documentation
Straightforward improvements here would be updating the links in the main README.md to the most up-to-date versions of their targets (e.g., clicking on "FragmentHeader description" from the home page points you to the v4 version of the fragment header, whereas we're up to v6. Also, not all the diagrams are completely up to date - e.g., here, it's v5 of FragmentHeader which is displayed.
Less importantly, it might be nice to explain what "component requests" are in a short sentence when discussing the TriggerRecordHeader.
As far as in-source commenting, I think it can be reduced in some places. E.g., comments like this
/**
* @brief Fragment destructor
*/
inline ~Fragment();
are probably overkill, as is
/**
* @brief Set the run_number for the Fragment
* @param run_number Value of run_number to set
*/
void set_run_number(run_number_t run_number) { header_()->run_number = run_number; }
Additionally, I believe that for one-line comments we want in Doxygen, the /// @ brief syntax can be a bit more terse than /**.
Relatedly, in CMakeLists.txt, there's some superfluous commenting - e.g.
##############################################################################
# Plugins
...when the package doesn't have any plugins.
Static assertions using offsetof to check byte location
Obviously, the effects of unexpected byte padding introduced by the compiler into structs could be disastrous when they get read back, so these assertions are very valuable for checking alignment, etc. These should be consistently applied, and in fact this package does a good job with this. As an aesthetic choice it may make sense to put them in a separate detail/*.hxx file; currently this is only done for SourceID. I'll only mention it here and not in the per-file comments, but something to consider.
Unit tests
These are very good; no major issues.
Per-file breakdown
Fragment.hpp
inline is used at the point of the member function declarations rather than their definitions; not life or death, but typically it's instead put at the definition location as it's considered an implementation detail.
- It's good that the copy and move semantics are made explicit; having said that, they and the destructor should be moved below the "normal" functions (
get_header(), etc.) as described here.
- Comments like
///< Fragment copy constructor is deleted aren't needed where the code is self-explanatory
- It may be worth considering dropping the
kReadOnlyMode option in that it doesn't seem to be used anywhere in our codebase (outside of unit tests and python bindings). The remaining two enums (kTakeOverBuffer and kCopyFromBuffer) could be generalized, put in Types.hpp, and also used by TriggerRecordHeader (see my comments later)
- The
Fragment::Fragment(void* existing_fragment_buffer,... constructor should be explicit about what it means for existing_fragment_buffer to be a nullptr. This could be documenting above the constructor "existing_fragment_buffer as a nullptr is a developer error and will cause a crash", asserting against this, throwing, etc...
- More a judgement call than anything, but it may also be worth considering dropping the
Fragment(void* buffer, size_t size); constructor as this is only used in unit tests (dfmodules, hdf5libs) and is simply a special case of Fragment(const std::vector<std::pair<void*, size_t>>& pieces)
- Speaking of
Fragment(const std::vector<std::pair<void*, size_t>>& pieces):
- Could make
size be of type fragment_size_t (from Types.hpp)
- Given how
size is calculated, size < sizeof(FragmentHeader) really isn't necessary, and if this is meant to capture overflows it would only capture the tiny fraction which result in a value from 0 to sizeof(FragmentHeader)
std::invalid_argument("The Fragment buffer point to NULL."); can be thrown without the memory allocated to m_data_arr being freed up
set_header_fields: the implementation is both clear about what's happening but vulnerable if new variables/getters are added to the FragmentHeader; an alternative might be to (1) save the size value in the original header, (2) clobber it with the passed header via = assignment, (3) reassign the original size value.
- JCF: need to think about this Does
Fragment need a version? Could be a passthrough to the FragmentHeader version.
FragmentHeader.hpp
- Some of the documentation is a bit redundant - e.g.,
FragmentHeader struct definition at the top of the file, or By default, all status bits are unset. Solutions differ, though - for the former case, maybe a mention/reminder of what the FragmentHeader is, while for the latter, it can just be removed since s_default_status_bits = 0 clearly implies By default, all status bits are unset.
- I can't recall but I would guess this is the result of nomenclature change at some point - "element ID" seems to be an alternate name for "SourceID"? E.g.,
SourceID element_id;. We could just go with SourceID source_id; (unless an element is a kind of source, in which case a quick comment might be helpful).
- Can remove the commented-out padding
- Any sense in having a
detector_id_t, adding it to Types.hpp, and using it in what's currently the uint16_t detector_id; declaration?
- In the
FragmentType enum, the comments for kFW_TriggerPrimitive and kTriggerPrimitive are extensive enough that it may be worth considering moving them above the body of the enum, as it makes the enum list harder to read.
get_fragment_type_names doesn't seem like it needs to be a function since it just wraps a std::map that could be declared in the unnamed namespace {}
Types.hpp
- Should be fine making
TypeDefaults a struct rather than class. In fact, we can go further: these static constexpr defaults could be taken out of a class/struct and could just all be wrapped in a namespace called typedefaults. There wouldn't be a loss of safety; these are all constexpr and you'd just be swapping TypeDefaults::<some default> for typedefaults::<some default>
- Many of the comments are pretty self-explanatory and could be removed (
* @brief Type used to represent run number, etc.)
* https://github.com/DUNE-DAQ/trgdataformats/blob/develop/include/trgdataformats/TriggerCandidateData.hpp#L22 in comment on trigger_type_t has the correct line, but that #L22 could easily break since daqdataformats doesn't otherwise depend on trgdataformats - just a thought.
TimeSliceHeader.hpp
- Could add a version check to the
static_asserts
- Could add an
is_in_valid_state function which checks the timeslice_header_marker, whether its member variables (timeslice_number, etc.) have valid values, and whether version matches s_timeslice_header_version
TimeSlice.hpp
- See earlier comment about use of
inline in declarations
- copy/move + destructor functions should be moved later in the class declaration and probably don't need redundant comments
- The functionality in
set_fragments is only used in dfmodules' TPBundleHandler::get_timeslice function, right after a TimesSlice instance is constructed - perhaps its functionality could be added to a constructor, with its argument as well?
- Could have an
is_in_valid_state, which may largely just be a passthrough to the TimeSliceHeader::is_in_valid_state function I suggest above
- In
get_total_size_bytes() and get_sum_of_fragment_payload_sizes(), may be worth considering using fragment_size_t here
SourceID.hpp
Subsystem_t may be overkill, since it's only used in one place, to define the type of the Subsystem enum class
- Use of
inline chould be moved from declarations to definitions
- JCF: check this There may be slightly more modern/efficient ways to implement those comparisons (spaceship operator, e.g.)
TriggerRecord.hpp
TriggerRecord::set_fragments isn't used in our codebase outside of its own unit tests; drop?
TriggerRecord::get_sum_of_fragment_payload_sizes not used in our codebase outside of its own unit tests and Python bindings; drop?
TriggerRecord::set_header not used in our codebase outside of its own unit test and (already commented-out) Python bindings; drop?
- copy/move functions and destructor should be repositioned; consider removing some unneeded comments
TriggerRecordHeader.hpp
- In
TriggerRecordHeader::TriggerRecordHeader(const std::vector<ComponentRequest>& components), the loop over components at the bottom is unnecessary since C++ guarantees that the elements of a std::vector are contiguous in memory
TriggerRecordHeader::TriggerRecordHeader(void* existing_trigger_record_header_buffer, bool copy_from_buffer) there are a few improvements which could be made
copy_from_buffer, as a boolean, means constructor calls aren't self-documenting (e.g., if I read TriggerRecordHeader(header_buffer, true) it's not immediately clear what true means). A simple enum (kTakeOverBuffer and kCopyFromBuffer) could make this clearer, plus as mentioned earlier be declared in Types.hpp and used in common with Fragment.
existing_trigger_record_header_buffer isn't checked for null
- If
TriggerRecordHeaderData were to have an is_in_valid_state function it could be checked before the resulting use of the passed bytes (header->num_requested_components, etc.) - more on this in a moment
- Both
at and operator[] are defined, but given that they both do the same thing (bounds-checked access), it seems only one should be retained. Relevant here is that in C++ containers, at is bounds-checked, and operator[] isn't.
TriggerRecordHeaderData.hpp
- As mentioned above, and
is_in_valid_state function could be useful. Among other things it could compare trigger_record_header_marker to s_trigger_record_header_magic, version to s_trigger_record_header_version, etc.
- Comments in lines like
kUnassigned9 = 9, ///< Status bit 9 is not assigned seem like overkill
fragment.cpp
- Available in
FragmentHeader.hpp but lacking bindings: kMPD, get_fragment_type_names, kUnassigned* (see Alessandro's comment), kInvalid
- Available in
Fragment.hpp but lacking bindings: all the set_ functions but this is deliberate.
Overview
This is a code review as part of the larger project of reviewing the various data formats packages, as described in https://github.com/DUNE-DAQ/daq-deliverables/issues/215. For details / motivations behind my suggestions, please click the link. In this Issue, I'll start by providing some general comments about the repository, then I'll go file-by-file.
General Comments
Documentation
Straightforward improvements here would be updating the links in the main
README.mdto the most up-to-date versions of their targets (e.g., clicking on "FragmentHeader description" from the home page points you to the v4 version of the fragment header, whereas we're up to v6. Also, not all the diagrams are completely up to date - e.g., here, it's v5 ofFragmentHeaderwhich is displayed.Less importantly, it might be nice to explain what "component requests" are in a short sentence when discussing the
TriggerRecordHeader.As far as in-source commenting, I think it can be reduced in some places. E.g., comments like this
are probably overkill, as is
Additionally, I believe that for one-line comments we want in Doxygen, the
/// @ briefsyntax can be a bit more terse than/**.Relatedly, in
CMakeLists.txt, there's some superfluous commenting - e.g....when the package doesn't have any plugins.
Static assertions using
offsetofto check byte locationObviously, the effects of unexpected byte padding introduced by the compiler into structs could be disastrous when they get read back, so these assertions are very valuable for checking alignment, etc. These should be consistently applied, and in fact this package does a good job with this. As an aesthetic choice it may make sense to put them in a separate
detail/*.hxxfile; currently this is only done forSourceID. I'll only mention it here and not in the per-file comments, but something to consider.Unit tests
These are very good; no major issues.
Per-file breakdown
Fragment.hppinlineis used at the point of the member function declarations rather than their definitions; not life or death, but typically it's instead put at the definition location as it's considered an implementation detail.get_header(), etc.) as described here.///< Fragment copy constructor is deletedaren't needed where the code is self-explanatorykReadOnlyModeoption in that it doesn't seem to be used anywhere in our codebase (outside of unit tests and python bindings). The remaining two enums (kTakeOverBufferandkCopyFromBuffer) could be generalized, put inTypes.hpp, and also used byTriggerRecordHeader(see my comments later)Fragment::Fragment(void* existing_fragment_buffer,...constructor should be explicit about what it means forexisting_fragment_bufferto be anullptr. This could be documenting above the constructor "existing_fragment_buffer as a nullptr is a developer error and will cause a crash",asserting against this, throwing, etc...Fragment(void* buffer, size_t size);constructor as this is only used in unit tests (dfmodules,hdf5libs) and is simply a special case ofFragment(const std::vector<std::pair<void*, size_t>>& pieces)Fragment(const std::vector<std::pair<void*, size_t>>& pieces):sizebe of typefragment_size_t(fromTypes.hpp)sizeis calculated,size < sizeof(FragmentHeader)really isn't necessary, and if this is meant to capture overflows it would only capture the tiny fraction which result in a value from 0 tosizeof(FragmentHeader)std::invalid_argument("The Fragment buffer point to NULL.");can be thrown without the memory allocated tom_data_arrbeing freed upset_header_fields: the implementation is both clear about what's happening but vulnerable if new variables/getters are added to theFragmentHeader; an alternative might be to (1) save the size value in the original header, (2) clobber it with the passed header via=assignment, (3) reassign the original size value.Fragmentneed aversion? Could be a passthrough to theFragmentHeaderversion.FragmentHeader.hppFragmentHeader struct definitionat the top of the file, orBy default, all status bits are unset. Solutions differ, though - for the former case, maybe a mention/reminder of what theFragmentHeaderis, while for the latter, it can just be removed sinces_default_status_bits = 0clearly impliesBy default, all status bits are unset.SourceID element_id;. We could just go withSourceID source_id;(unless an element is a kind of source, in which case a quick comment might be helpful).detector_id_t, adding it toTypes.hpp, and using it in what's currently theuint16_t detector_id;declaration?FragmentTypeenum, the comments forkFW_TriggerPrimitiveandkTriggerPrimitiveare extensive enough that it may be worth considering moving them above the body of the enum, as it makes the enum list harder to read.get_fragment_type_namesdoesn't seem like it needs to be a function since it just wraps astd::mapthat could be declared in the unnamednamespace {}Types.hppTypeDefaultsastructrather thanclass. In fact, we can go further: thesestatic constexprdefaults could be taken out of a class/struct and could just all be wrapped in a namespace calledtypedefaults. There wouldn't be a loss of safety; these are allconstexprand you'd just be swappingTypeDefaults::<some default>fortypedefaults::<some default>* @brief Type used to represent run number, etc.)* https://github.com/DUNE-DAQ/trgdataformats/blob/develop/include/trgdataformats/TriggerCandidateData.hpp#L22in comment ontrigger_type_thas the correct line, but that#L22could easily break sincedaqdataformatsdoesn't otherwise depend ontrgdataformats- just a thought.TimeSliceHeader.hppstatic_assertsis_in_valid_statefunction which checks thetimeslice_header_marker, whether its member variables (timeslice_number, etc.) have valid values, and whetherversionmatchess_timeslice_header_versionTimeSlice.hppinlinein declarationsset_fragmentsis only used indfmodules'TPBundleHandler::get_timeslicefunction, right after aTimesSliceinstance is constructed - perhaps its functionality could be added to a constructor, with its argument as well?is_in_valid_state, which may largely just be a passthrough to theTimeSliceHeader::is_in_valid_statefunction I suggest aboveget_total_size_bytes()andget_sum_of_fragment_payload_sizes(), may be worth considering usingfragment_size_thereSourceID.hppSubsystem_tmay be overkill, since it's only used in one place, to define the type of theSubsystemenum classinlinechould be moved from declarations to definitionsTriggerRecord.hppTriggerRecord::set_fragmentsisn't used in our codebase outside of its own unit tests; drop?TriggerRecord::get_sum_of_fragment_payload_sizesnot used in our codebase outside of its own unit tests and Python bindings; drop?TriggerRecord::set_headernot used in our codebase outside of its own unit test and (already commented-out) Python bindings; drop?TriggerRecordHeader.hppTriggerRecordHeader::TriggerRecordHeader(const std::vector<ComponentRequest>& components), the loop over components at the bottom is unnecessary since C++ guarantees that the elements of astd::vectorare contiguous in memoryTriggerRecordHeader::TriggerRecordHeader(void* existing_trigger_record_header_buffer, bool copy_from_buffer)there are a few improvements which could be madecopy_from_buffer, as a boolean, means constructor calls aren't self-documenting (e.g., if I readTriggerRecordHeader(header_buffer, true)it's not immediately clear whattruemeans). A simple enum (kTakeOverBufferandkCopyFromBuffer) could make this clearer, plus as mentioned earlier be declared inTypes.hppand used in common withFragment.existing_trigger_record_header_bufferisn't checked for nullTriggerRecordHeaderDatawere to have anis_in_valid_statefunction it could be checked before the resulting use of the passed bytes (header->num_requested_components, etc.) - more on this in a momentatandoperator[]are defined, but given that they both do the same thing (bounds-checked access), it seems only one should be retained. Relevant here is that in C++ containers,atis bounds-checked, andoperator[]isn't.TriggerRecordHeaderData.hppis_in_valid_statefunction could be useful. Among other things it could comparetrigger_record_header_markertos_trigger_record_header_magic,versiontos_trigger_record_header_version, etc.kUnassigned9 = 9, ///< Status bit 9 is not assignedseem like overkillfragment.cppFragmentHeader.hppbut lacking bindings:kMPD,get_fragment_type_names,kUnassigned*(see Alessandro's comment),kInvalidFragment.hppbut lacking bindings: all theset_functions but this is deliberate.