Skip to content

Conversation

@justonedev1
Copy link
Collaborator

@justonedev1 justonedev1 commented Jul 31, 2025

This is just first version of Data structure, that should be able to ingest everything. @knopers8 if you can take a look and say whether it is at least something to build upon.

Usage can be seen in SkeletonCheck.cxx

@justonedev1 justonedev1 force-pushed the QC-1298 branch 5 times, most recently from 6b6734d to c37c37e Compare August 7, 2025 17:50
@justonedev1
Copy link
Collaborator Author

justonedev1 commented Aug 7, 2025

@knopers8 version 2. I am not saying that it is 100% done, but it should fulfill everything we talked about, so you can take a look as most of the functionality is there.

@knopers8
Copy link
Collaborator

knopers8 commented Aug 8, 2025

For the record, we discussed the PR in person and agreed to extend the interface adoption to Aggregators and add support for inserting raw pointers. I will do a full review once we have these two points complete.

Copy link
Collaborator

@knopers8 knopers8 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks a lot! Indeed I am not happy that we leak some data storage details from Check and Aggregator into Data, but it seems we cannot avoid it. Otherwise, the interface is looking very promising.

I would ask you for the following:

  • See what could be moved to compiled source files. This interface will be included in hundreds of files, so it should be well optimized for compilation time. Consider having compiled versions for MonitorObject and QualityObject specializations.
  • See what could be hidden from the main header file into an .inl for easier reading by the users.
  • Prepare helpers (and feel free to disagree with the proposals):
    • iterateMonitorObjects()
    • iterateMonitorObjects(std::string_view taskName)
    • getMonitorObject<StoredType = MonitorObject>(std::string_view objectName, std::string_view taskName) // no risk of obj name collisions
    • getMonitorObject<StoredType = MonitorObject>(std::string_view objectName) // gets first matching MO!!!
    • `getMonitorObject(std::string_view objectName) // gets the underlying object if StoredType is anything else than MonitorObject
    • getMonitorObject<StoredType>(std::string_view objectName, std::string_view taskName) // like above, but no risk of obj name collisions
    • similar helpers for QOs
    • to be discussed also with Barth: maybe these helpers should be a part of Data. the advantage is that users won't have to pass Data as an argument. The disadvantage is polluting the interface...

@justonedev1
Copy link
Collaborator Author

okay, thanks I will add the helpers and address other necessary additions.

@justonedev1 justonedev1 force-pushed the QC-1298 branch 5 times, most recently from 5597c28 to a0ae926 Compare August 14, 2025 07:18
@justonedev1 justonedev1 changed the title WIP [QC-1298] Extensible interface for accessing QC data sources [QC-1298] Extensible interface for accessing QC data sources Aug 14, 2025
@justonedev1 justonedev1 requested a review from knopers8 August 15, 2025 11:14
Copy link
Collaborator

@knopers8 knopers8 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, just a few more suggestions, but this looks already good.

I propose that on Tuesday QC weekly we brainstorm the three of us:

  • interface names
  • whether to move helpers to Data or keep it in DataAdapters
  • whether returning std::optional<std::reference_wrapper> is the easiest and safest we can do for the users.

@Barthelemy Could you perhaps have a look as well?

Comment on lines 72 to 83
template <typename T>
static constexpr auto any_to_specific = std::views::transform([](const auto& pair) -> std::pair<std::string_view, const T*> { return { pair.first, any_cast_try_shared_raw_ptr<T>(pair.second) }; });

static constexpr auto filter_nullptr_in_pair = std::views::filter([](const auto& pair) -> bool { return pair.second != nullptr; });

static constexpr auto filter_nullptr = std::views::filter([](const auto* ptr) -> bool { return ptr != nullptr; });

static constexpr auto pair_to_reference = std::views::transform([](const auto& pair) -> const auto& { return *pair.second; });

static constexpr auto pair_to_value = std::views::transform([](const auto& pair) -> const auto* { return pair.second; });

static constexpr auto pointer_to_reference = std::views::transform([](const auto* ptr) -> const auto& { return *ptr; });
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

consider breaking into multiple lines for easier readability, e.g.

template <typename T>
static constexpr auto any_to_specific = std::views::transform(
  [](const auto& pair) -> std::pair<std::string_view, const T*> {
    return { pair.first, any_cast_try_shared_raw_ptr<T>(pair.second) }; 
  });


static constexpr auto filter_nullptr = std::views::filter([](const auto* ptr) -> bool { return ptr != nullptr; });

static constexpr auto pair_to_reference = std::views::transform([](const auto& pair) -> const auto& { return *pair.second; });
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
static constexpr auto pair_to_reference = std::views::transform([](const auto& pair) -> const auto& { return *pair.second; });
static constexpr auto pair_to_value_const_ref = std::views::transform([](const auto& pair) -> const auto& { return *pair.second; });

return result;
}

const TH1& histogram = histOpt.value().get();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have to admit I am not a great fan of .value().get(), but I don't see a better option at the moment than std::reference_wrapper. This being said, here it should automatically cast to const TH1& without get() and if we go this way, the examples should use implicit casting.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree

Copy link
Collaborator Author

@justonedev1 justonedev1 Aug 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with the implicit operator... I was using explicit here for 2 reasons: I overlooked implicit operator in docs and I personally prefer to use explicit calls, so I am not surprised how something happened (eg. std::optional.has_value(), .value(), or comparing raw pointers to nullptr instead of just using !)...

One thing that I would like to ask is: should I use explicit type (const TH1& in this case), or const auto&? What do you think is better for the example for users?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

according to my trials on godbolt, const auto& was not enough to invoke implicit cast, so I think we have to use const TH1&. Perhaps the first case it is used in SkeletonCheck and SkeletonAggregator should be annotated with a comment that we are in fact casting from a reference wrapper.

template <typename StoredType = MonitorObject>
std::optional<std::reference_wrapper<const StoredType>> getMonitorObject(const Data& data, std::string_view objectName);

inline auto iterateQualityObjects(const Data& data);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think we need also inline auto iterateQualityObjects(const Data& data, std::string_view checkName);. One Check may return several QOs if the policy OnEachSeparately is selected.

Copy link
Collaborator

@Barthelemy Barthelemy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you, this is very nice. Really great !

I have put a few comments and we will discuss it during this afternoon's meeting.
In general, I would appreciate if there were more comments to explain what the intent is and why it is done this or that way.

I am also pondering the benefit of having a separate file for the inline stuff.


// Override interface
void configure() override;
Quality check(std::map<std::string, std::shared_ptr<MonitorObject>>* moMap) override;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why have both check methods ?
Do we want users to have both ?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to clarify what I mean: I would expect users to only implement the check that takes data if they write a new Check or if they want to move to the new interface.
And if they don't want to do anything, then they just keep the old implementation.

return result;
}

const TH1& histogram = histOpt.value().get();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree

// or submit itself to any jurisdiction.

///
/// \file Data.h
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a very generic name

concept invocable_r = std::invocable<Function, Args...> && std::same_as<std::invoke_result_t<Function, Args...>, Result>;

template <typename ContainerMap>
class DataGeneric
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps you could have a comment to explain what its purpose is and how it fits in the QC .

}
};

using transparent_unordered_map = std::unordered_map<std::string, std::any, StringHash, std::equal_to<>>;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A bit of explanation of what we are doing here would be helpful.

// or submit itself to any jurisdiction.

///
/// \file Data.inl
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As for Data.h, it would be helpful to have a bit more comments about the goal and intent of these functions.

@justonedev1
Copy link
Collaborator Author

justonedev1 commented Aug 19, 2025

@Barthelemy

I have put a few comments and we will discuss it during this afternoon's meeting. In general, I would appreciate if there were more comments to explain what the intent is and why it is done this or that way.

I will add more comments and documentation about everything when the code is finalised. Neither me nor Piotr were sure about naming, so if you don't like some naming go ahead and propose better names, this was built as a POC, so naming might be all over the place as it was being changed as I went along and I didn't make "finalising pass" to check if the names are as best as they can be

I am also pondering the benefit of having a separate file for the inline stuff.

Do you mean .inl files?

@knopers8
Copy link
Collaborator

I am also pondering the benefit of having a separate file for the inline stuff.

Do you mean .inl files?

@Barthelemy I asked Michal to do it and the reason was to make this header as much readable as possible for the users, so they are not lost in the implementation details when checking how they can use the interface.

@Barthelemy
Copy link
Collaborator

@Barthelemy I asked Michal to do it and the reason was to make this header as much readable as possible for the users, so they are not lost in the implementation details when checking how they can use the interface.

@knopers8 ok, understood.

@justonedev1 justonedev1 requested a review from knopers8 August 27, 2025 14:16
Copy link
Collaborator

@knopers8 knopers8 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am wondering if we still need QCInputsGeneric, now that you found the most suitable container and it's unlikely we would change it or use more than one. What do you think about merging it back with QCInputs to simplify the code?

virtual ~CheckInterface() = default;

/// \brief Returns the quality associated with these objects.
///
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please add a note that users should prefer the other one and this one is for backwards-compatibility.

/// \returns Optional reference to const Result if found desired item of type Result.
/// \par Example
/// \code{.cpp}
/// if (auto opt = data.get<unsigned>("count")) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i would propose to use MonitorObject in examples so they are more real-life.

/// \returns Range of const references to stored Result instances.
/// \par Example
/// \code{.cpp}
/// for (auto& val : data.iterateByResultype<unsigned>()) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the example uses a different method name

/// \brief Transparent hash functor for string and string_view.
///
/// Enables heterogeneous lookup in unordered maps keyed by std::string.
struct StringHash {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps this could be moved to inl as well? struct StringHash; declaration might be only needed.

inline auto iterateMonitorObjects(const QCInputs& data, std::string_view taskName);

/// \brief Retrieve the first MonitorObject of type StoredType matching both name and task.
/// \tparam StoredType Type of MonitorObject or subclass to retrieve.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"subclass" typically means "derived class", how about "stored class" or "wrapped class"?

There are other uses of "subclass" in this file".

inline auto iterateQualityObjects(const QCInputs& data, std::string_view checkName)
{
const auto filterQOByName = [checkName](const auto& pair) {
return std::string_view(pair.second->getName()) == checkName;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use getCheckName. getName can be different than getCheckName when OnEachSeparately update policy was used for a Check. In such case, a separate QO is produced for each MO that is an input, so you get CheckA/MO1, Check/MO2, ... See the implementation of QualityObject::getName

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was looking at it and I thought that for some reason you want to specifically use getName and not getCheckName... that is my bad, sorry

namespace o2::quality_control::core
{

std::optional<std::reference_wrapper<const QualityObject>> getQualityObject(const QCInputs& data, std::string_view objectName)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The declaration mentions checkName as the argument, here it's objectName. These might not be the same as mentioned earlier. Consequently, in the implementation, use getCheckName.

for (const auto& item : qoMap) {
ILOG(Info, Devel) << "Object: " << (*item.second) << ENDM;
}
ILOG(Info, Devel) << " received a data of size : " << data.size() << ENDM;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
ILOG(Info, Devel) << " received a data of size : " << data.size() << ENDM;
ILOG(Info, Devel) << " received data of size : " << data.size() << ENDM;

Data is plural of datum.

@justonedev1
Copy link
Collaborator Author

I am wondering if we still need QCInputsGeneric, now that you found the most suitable container and it's unlikely we would change it or use more than one. What do you think about merging it back with QCInputs to simplify the code?

Problem with that is that I would need to delete the benchmark code as well, which would mean that the reason for unordered_map usage might be lost in time and other containers that were tried would be certainly forgotten. But if you think that this is ok or not necessary to keep, I will delete it.

@knopers8
Copy link
Collaborator

knopers8 commented Sep 8, 2025

I am wondering if we still need QCInputsGeneric, now that you found the most suitable container and it's unlikely we would change it or use more than one. What do you think about merging it back with QCInputs to simplify the code?

Problem with that is that I would need to delete the benchmark code as well, which would mean that the reason for unordered_map usage might be lost in time and other containers that were tried would be certainly forgotten. But if you think that this is ok or not necessary to keep, I will delete it.

I think you can leave the benchmark for the selected container and add a comment next to using container_t = std::unordered_map<std::string, std::any, StringHash, std::equal_to<>> which mentions that it was the fastest from x, y and z. You can also leave the output from benchmarks as an attachment to the ticket.

@justonedev1 justonedev1 requested a review from knopers8 September 8, 2025 15:58
Copy link
Collaborator

@knopers8 knopers8 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you!

@knopers8 knopers8 enabled auto-merge (squash) September 9, 2025 06:53
@knopers8 knopers8 merged commit 87762a0 into master Sep 10, 2025
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

4 participants