Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@marquiz
Copy link
Contributor

@marquiz marquiz commented Oct 24, 2025

This patch is an attempt to improve the filtering api of the cstates package, make it more extensible and future proof and less kludge-ish.

Changes explained:

  1. Change the "filter" function type into an interface. More flexibility for future extensions and in setting up the filter.
  2. Pass the input for evaluation as a struct instead of args. Makes future extensions to the "args" more flexible, without breaking existing filters.
  3. Only take a single filter as an argument for the cstate functions (instead of passing multiple filters). This will remove the possible confusion on how multiple filters are combined (e.g. accidental disjoint sets like (FilterCPUs(1), FilterCPUs(10), ...).
  4. Combine the separate filter functions into one "basic filter". Result of 3 above (single filter arg).

@marquiz
Copy link
Contributor Author

marquiz commented Oct 24, 2025

I had some no-network time on a flight so I took a quick stab at this. A sort-of academic exercise :)

Tell me what you think. Things could be done done in different ways, like make the filter immutable (like NewBasicFilter(opts...)), keep the function type instead of interface etc...

Copy link
Contributor

@askervin askervin left a comment

Choose a reason for hiding this comment

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

@marquiz, I really like this! Yet I was worried about the loss of expressive power (negations and parameter combinations), I think this solves the problem by saying "this is the BasicFilter, and filter.go can be extended with MoreComplexFilter laterj, if needed, without breaking existing code". So it really looks future-proof, as you say.

The only question that I have would be regarding asymmetry in constructing BasicFilters and FilterInputs. While the first are defined with

NewBasicFilter().SetCPUs(...).SetCstateNames(...)....

The latter are with

NewFilterInput(WithCPU(...), WithCstateName(...), ....)

The API would be more confusing if NewBasicFilter(WithCPUs(...), ...) would be possible because then we would have WithCPU() that would work with FilterInput and WithCPUs() that would work with BasicFilter. But I don't see why it couldn't be the other way around, that is, we would do

NewFilterInput().SetCPU(...).SetCstateName(...)

Did you already consider and possibly already reject this kind of symmetry in BasicFilter and FilterInput?

@marquiz
Copy link
Contributor Author

marquiz commented Oct 24, 2025

But I don't see why it couldn't be the other way around, that is, we would do

NewFilterInput().SetCPU(...).SetCstateName(...)

Did you already consider and possibly already reject this kind of symmetry in BasicFilter and FilterInput?

Good point and I don't disagree. The primary reason for this asymmetry is that the NewFilterInput was originally not exported, only for internal use of the cstates package but I made it exported just before submission (e.g. could be used for testing by external MoreComplicatedFilters. I was playing around with different styles and this was a remnant of the evolution. I agree that using the opts pattern (withXYZ()) for both would probably be confusing (unless the filter and inputs were in separate packages). I'll probably change the filter input to use the SetXYZ() style.

One gap to tackle is the case where no filtering is needed. Simplest would be allow nil value for the filter (and document that the Match() method of the filter must accept nil receiver. Other options that come to mind are the varargs style (multiple filters, similar to now) or opts vararg (like withFilter()).

Let's see if @klihub has some insights...

@marquiz
Copy link
Contributor Author

marquiz commented Oct 28, 2025

Updated. The FilterInput usage is now similar to the Filter usage (like NewFilterInput().SetCPU(2).SetCstateName("C2")). This is now a separate patch but I'll squash it if you think it looks good. I didn't do anything for the "no filter" case - couldn't decide what would be the best approach so I left it out as a possible future improvement.

@askervin @klihub

Copy link
Contributor

@askervin askervin left a comment

Choose a reason for hiding this comment

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

Suggesting some comments based on porting Balloons cstates on top of this.

With suggested changes, balloons cstates e2e test passes. This API feels very nice in real use. :)

return true
}
func (f *BasicFilter) evaluateCPU(cpu *utils.ID) bool {
return cpu == nil || len(f.cpus) == 0 || f.cpus[*cpu]
Copy link
Contributor

Choose a reason for hiding this comment

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

If user has created NewBasicFilter().SetCPUs(), it should mean that none of the CPUs match. That is, while f.cpus == nil means we do not filter based on CPUs at all, f.cpus != nil && len(f.cpus) == 0 should mean that we filter based on CPUs, and we will not accept any CPU.

Suggestion: replace len(f.MAP) == 0 with f.MAP == nil in all evaluate...() functions.

I ran into this issue when porting current Balloons cstates draft on top of this patch. The draft takes 'disabledCstates' from user input, and creates a filtered copy like
onlyDisabled := all.Copy(NewBasicFilter().SetCstateNames(disabledCstates...))
If disabledCstates included no c-states, current filtering copied all cstates, yet it should not have copied any, and disabled all cstates as a consequence.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hmm, thanks for pointing this out. You're absolutely right and what you described was the original intention (usage of the pointer values) but I don't know why I used then length check. Will change.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed as suggested

return !filter(cpuid, cstateName, attr, val)
}
func (f *BasicFilter) evaluateAttribute(attr *AttrID) bool {
return attr == nil || len(f.attributes) == 0 || f.attributes[*attr] != nil
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggestion: for clarity and robust implementation, let's have separate maps:

f.attributes map[AttrID]bool
f.attributeValues map[AttriID]map[string]bool

Trying to piggyback attribute-value filtering and attribute-only filtering into the same data structure leads into trouble. Currently we cannot distinguish between:

  1. NewBasicFilter().SetAttribute(AttrDisable)
  2. NewBasicFilter().SetAttributeValue(AttrDisable, emptySliceOfAcceptableValues...)

All disable values should pass the first filter, while no disable value should pass the second. Furthermore, the first filter should filter out all other but the disable attribute, while the second should allow reading all attributes and filter only based on observed disable values. However, current implementation creates exactly the same structure from these, that is f.attributes[AttrDisable] == map[string]bool{}.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah, good good. I was actually struggling with this -- was back-and-forth with separate maps or single map -- as I was unsure what the preferable functionality for "filter attribute values" is. I.e. should it filter out other attributes or not. Will change this.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

second should allow reading all attributes and filter only based on observed disable values

Hmm, this^ what you suggest actually breaks e.g. this test case:

c2Disabled := cs.Copy(FilterNames("C2", "C3", "C4"), FilterAttrValues(AttrDisable, "1"))

Implementing your suggestion accepts any value for other attributes than AttrDisable -> returns all CPUs (that have the cstates specified). To make it work it would need to be like:
cs.Copy(NewBasicFilter().SetCstateNames("C2", "C3", "C4").SetAttributes(AttrDisable).SetAttributeValues(AttrDisable, "1"))

Is this logical? Maybe(?) Anyway, it would change the filtering from your original impl.

Copy link
Contributor

@askervin askervin Oct 29, 2025

Choose a reason for hiding this comment

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

I think it is indeed more logical to fix the test as you suggest, that is:

cs.Copy(NewBasicFilter().SetCstateNames("C2", "C3", "C4").SetAttributes(AttrDisable).SetAttributeValues(AttrDisable, "1"))

Original filter implementation didn't make clear distinction between attribute and attribute-value filtering. No wonder it made you go back-and-forth in this design clean-up, too. I think it'll be easier to understand when these two filters do not interfere.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Check, let's do it this way. I'll change the impl and tests and try to document it a bit better. I'll also add some unit tests for the BasicFilter itself.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done. Unit tests added

This patch is an attempt to improve the filtering api of the cstates
package, make it more extensible and future proof and less kludge-ish.

Changes explained:

1. Change the "filter" function type into an interface. More flexibility
   for future extensions and in setting up the filter.
2. Pass the input for evaluation as a struct instead of args. Makes
   future extensions to the "args" more flexible, without breaking
   existing filters.
3. Only take a single filter as an argument for the cstate functions
   (instead of passing multiple filters). This will remove the possible
   confusion on how multiple filters are combined (e.g. accidental
   disjoint sets like (FilterCPUs(1), FilterCPUs(10), ...).
4. Combine the separate filter functions into one "basic filter". Result
   of intel#3 (single filter arg).
@marquiz marquiz force-pushed the devel/cstate-filter branch from ebacf65 to 377067c Compare October 30, 2025 09:53
@marquiz marquiz changed the title [RFC] cstates: rework filtering API cstates: rework filtering API Oct 30, 2025
@marquiz
Copy link
Contributor Author

marquiz commented Oct 30, 2025

Update: rebased, squashed, addressed review comments, added unit tests for BasicFilter, removed RFC status

@askervin @klihub

@marquiz marquiz requested a review from askervin October 30, 2025 10:02
Copy link
Contributor

@askervin askervin left a comment

Choose a reason for hiding this comment

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

LGTM.

Thanks @marquiz! This is a very nice improvement to filtering, and it fixed unclear semantics of the original API!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants