Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add optional matcher to IndexReader.LabelValues #3391

Merged
merged 3 commits into from
Nov 7, 2022

Conversation

colega
Copy link
Contributor

@colega colega commented Nov 4, 2022

What this PR does

More often than not we retrieve label values to filter them using a matcher later. If we pass in the matcher as a filter function, we can avoid moving some extra values.

This shows a -5% cpu improvement when this path is hit, and no change otherwise.

name                                                               old time/op  new time/op  delta
BucketIndexReader_ExpandedPostings/n="1"                           7.27ms ± 2%  7.24ms ± 2%    ~     (p=0.447 n=9+10)
BucketIndexReader_ExpandedPostings/n="1",j="foo"                   32.9ms ± 5%  32.5ms ± 1%    ~     (p=0.236 n=9+8)
BucketIndexReader_ExpandedPostings/j="foo",n="1"                   32.5ms ± 1%  32.5ms ± 2%    ~     (p=0.631 n=10+10)
BucketIndexReader_ExpandedPostings/n="1",j!="foo"                  28.3ms ± 2%  28.0ms ± 2%    ~     (p=0.089 n=10+10)
BucketIndexReader_ExpandedPostings/i=~".*"                          142ms ± 5%   135ms ± 3%  -5.03%  (p=0.000 n=10+10)
BucketIndexReader_ExpandedPostings/i=~".+"                          374ms ± 3%   371ms ± 4%    ~     (p=0.579 n=10+10)
BucketIndexReader_ExpandedPostings/i=~""                            366ms ± 3%   364ms ± 3%    ~     (p=0.529 n=10+10)
BucketIndexReader_ExpandedPostings/i!=""                            374ms ± 2%   370ms ± 4%    ~     (p=0.278 n=9+10)
BucketIndexReader_ExpandedPostings/n="1",i=~".*",j="foo"           35.5ms ± 2%  35.4ms ± 1%    ~     (p=0.905 n=10+9)
BucketIndexReader_ExpandedPostings/n="1",i=~".*",i!="2",j="foo"    43.2ms ± 5%  43.6ms ± 3%    ~     (p=0.280 n=10+10)
BucketIndexReader_ExpandedPostings/n="1",i!=""                      190ms ± 3%   178ms ± 2%  -6.41%  (p=0.000 n=10+9)
BucketIndexReader_ExpandedPostings/n="1",i!="",j="foo"              210ms ± 2%   192ms ± 1%  -8.71%  (p=0.000 n=10+9)
BucketIndexReader_ExpandedPostings/n="1",i=~".+",j="foo"            211ms ± 2%   193ms ± 1%  -8.48%  (p=0.000 n=10+10)
BucketIndexReader_ExpandedPostings/n="1",i=~"1.+",j="foo"          42.0ms ± 3%  40.5ms ± 2%  -3.71%  (p=0.000 n=10+9)
BucketIndexReader_ExpandedPostings/n="1",i=~".+",i!="2",j="foo"     214ms ± 2%   201ms ± 3%  -5.82%  (p=0.000 n=10+10)
BucketIndexReader_ExpandedPostings/n="1",i=~".+",i!~"2.*",j="foo"   239ms ± 1%   226ms ± 1%  -5.30%  (p=0.000 n=8+10)
BucketIndexReader_ExpandedPostings/i=~"0xxx|1xxx|2xxx"             32.7µs ± 1%  32.7µs ± 1%    ~     (p=0.897 n=10+8)
BucketIndexReader_ExpandedPostings/i=~"(0|1|2)xxx"                 32.7µs ± 2%  32.5µs ± 3%    ~     (p=0.280 n=10+10)
BucketIndexReader_ExpandedPostings/i=~"[0-2]xxx"                   32.6µs ± 1%  32.3µs ± 2%    ~     (p=0.101 n=8+10)
BucketIndexReader_ExpandedPostings/i!~[0-2]xxx                      151ms ± 3%   144ms ± 2%  -4.35%  (p=0.000 n=10+9)
BucketIndexReader_ExpandedPostings/i=~".*",_i!~[0-2]xxx             152ms ± 4%   149ms ± 1%  -2.30%  (p=0.009 n=10+10)
BucketIndexReader_ExpandedPostings/p!=""                           38.4ms ± 3%  39.3ms ± 2%  +2.31%  (p=0.004 n=10+10)

Before submitting this PR I tried to understand why is it really better?
I ran cpu profiling on the most improving case BucketIndexReader_ExpandedPostings/n="1",i!="",j="foo" on both versions, and zoomed in to the toPostingsGroup function:

cpu on main

image

cpu after this change

image

Since the extra time was clearly spent growing a slice, I validated two things:
First I thought that maybe inside of LabelValues we were not building enough space for the values:

https://github.com/grafana/mimir/blob/4d586af90d9c38cbe18a4b2cb0ede46b74a1e824/pkg/storegateway/indexheader/binary_reader.go#L884-L884

After double checking the logic itself, I just checked that this was not the case by adding a check at the end of that method and checking that it was not hit in the benchmarks:

	if len(values) > len(e.offsets)*r.postingOffsetsInMemSampling {
		panic("underallocated")
	}

Then I thought, okay, we're just growing the toAdd and toRemove in toPostingsGroup, so maybe by preallocating those to their worst case would solve that?

diff --git pkg/storegateway/bucket.go pkg/storegateway/bucket.go
index 7726b6649..9d3f0c49a 100644
--- pkg/storegateway/bucket.go
+++ pkg/storegateway/bucket.go
@@ -2003,7 +2003,7 @@ func toPostingGroup(lvalsFn func(name string) ([]string, error), m *labels.Match
 	// have the label name set too. See: https://github.com/prometheus/prometheus/issues/3575
 	// and https://github.com/prometheus/prometheus/pull/3578#issuecomment-351653555.
 	if m.Matches("") {
-		var toRemove []labels.Label
+		toRemove := make([]labels.Label, 0, len(vals))
 		for _, val := range vals {
 			if !m.Matches(val) {
 				toRemove = append(toRemove, labels.Label{Name: m.Name, Value: val})
@@ -2015,7 +2015,7 @@ func toPostingGroup(lvalsFn func(name string) ([]string, error), m *labels.Match
 
 	// Our matcher does not match the empty value, so we just need the postings that correspond
 	// to label values matched by the matcher.
-	var toAdd []labels.Label
+	toAdd := make([]labels.Label, 0, len(vals))
 	for _, val := range vals {
 		if m.Matches(val) {
 			toAdd = append(toAdd, labels.Label{Name: m.Name, Value: val})

However, I benchmarked that comparing it to both this PR (the results below: old is this PR, new is main with just the preallocated slice) and to main (results not posted, not really interesting, but you can infer them), and while it's comparable and even improving in some cases, it is much worse in some other:

name                                                               old time/op  new time/op  delta
BucketIndexReader_ExpandedPostings/n="1"                           7.24ms ± 2%  7.13ms ± 4%     ~     (p=0.123 n=10+10)
BucketIndexReader_ExpandedPostings/n="1",j="foo"                   32.5ms ± 1%  32.0ms ± 2%   -1.36%  (p=0.001 n=8+9)
BucketIndexReader_ExpandedPostings/j="foo",n="1"                   32.5ms ± 2%  32.4ms ± 1%     ~     (p=0.315 n=10+9)
BucketIndexReader_ExpandedPostings/n="1",j!="foo"                  28.0ms ± 2%  28.3ms ± 1%   +0.89%  (p=0.023 n=10+10)
BucketIndexReader_ExpandedPostings/i=~".*"                          135ms ± 3%   131ms ± 3%   -2.32%  (p=0.007 n=10+10)
BucketIndexReader_ExpandedPostings/i=~".+"                          371ms ± 4%   366ms ± 2%     ~     (p=0.123 n=10+10)
BucketIndexReader_ExpandedPostings/i=~""                            364ms ± 3%   355ms ± 2%   -2.42%  (p=0.000 n=10+10)
BucketIndexReader_ExpandedPostings/i!=""                            370ms ± 4%   367ms ± 4%     ~     (p=0.315 n=10+10)
BucketIndexReader_ExpandedPostings/n="1",i=~".*",j="foo"           35.4ms ± 1%  40.1ms ± 1%  +13.05%  (p=0.000 n=9+10)
BucketIndexReader_ExpandedPostings/n="1",i=~".*",i!="2",j="foo"    43.6ms ± 3%  47.8ms ± 1%   +9.64%  (p=0.000 n=10+10)
BucketIndexReader_ExpandedPostings/n="1",i!=""                      178ms ± 2%   179ms ± 2%     ~     (p=0.666 n=9+9)
BucketIndexReader_ExpandedPostings/n="1",i!="",j="foo"              192ms ± 1%   195ms ± 1%   +1.89%  (p=0.000 n=9+9)
BucketIndexReader_ExpandedPostings/n="1",i=~".+",j="foo"            193ms ± 1%   194ms ± 1%     ~     (p=0.055 n=10+8)
BucketIndexReader_ExpandedPostings/n="1",i=~"1.+",j="foo"          40.5ms ± 2%  42.9ms ± 2%   +6.03%  (p=0.000 n=9+9)
BucketIndexReader_ExpandedPostings/n="1",i=~".+",i!="2",j="foo"     201ms ± 3%   204ms ± 2%     ~     (p=0.165 n=10+10)
BucketIndexReader_ExpandedPostings/n="1",i=~".+",i!~"2.*",j="foo"   226ms ± 1%   230ms ± 2%   +1.38%  (p=0.004 n=10+9)
BucketIndexReader_ExpandedPostings/i=~"0xxx|1xxx|2xxx"             32.7µs ± 1%  32.9µs ± 3%     ~     (p=0.173 n=8+10)
BucketIndexReader_ExpandedPostings/i=~"(0|1|2)xxx"                 32.5µs ± 3%  32.9µs ± 2%   +1.33%  (p=0.043 n=10+10)
BucketIndexReader_ExpandedPostings/i=~"[0-2]xxx"                   32.3µs ± 2%  32.9µs ± 2%   +1.85%  (p=0.009 n=10+10)
BucketIndexReader_ExpandedPostings/i!~[0-2]xxx                      144ms ± 2%   145ms ± 4%     ~     (p=0.182 n=9+10)
BucketIndexReader_ExpandedPostings/i=~".*",_i!~[0-2]xxx             149ms ± 1%   149ms ± 2%     ~     (p=0.971 n=10+10)
BucketIndexReader_ExpandedPostings/p!=""                           39.3ms ± 2%  37.7ms ± 3%   -4.15%  (p=0.000 n=10+10)

I think this change makes sense and it's not making it much more complex, so we can go forward with it.

Which issue(s) this PR fixes or relates to

None, just was surfing the code.

Checklist

  • Tests updated
  • Documentation added
  • CHANGELOG.md updated - the order of entries should be [CHANGE], [FEATURE], [ENHANCEMENT], [BUGFIX]

@colega colega requested a review from a team as a code owner November 4, 2022 11:33
Copy link
Member

@pstibrany pstibrany left a comment

Choose a reason for hiding this comment

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

Thanks, this looks fine to me.

@@ -1963,7 +1964,7 @@ func checkNilPosting(l labels.Label, p index.Postings) index.Postings {
}

// NOTE: Derived from tsdb.postingsForMatcher. index.Merge is equivalent to map duplication.
func toPostingGroup(lvalsFn func(name string) ([]string, error), m *labels.Matcher) (*postingGroup, error) {
func toPostingGroup(lvalsFn func(name string, filter func(string) bool) ([]string, error), m *labels.Matcher) (*postingGroup, error) {
Copy link
Member

Choose a reason for hiding this comment

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

nit: I'd suggest to extract func(name string, filter func(string) bool) ([]string, error) into a separate type, to make it easier to see what is the type of lvalsFn, as this entire function signature is getting difficult to read.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've extracted a labelValuesReader interface that has only this function and now pass the indexHeader as an instance of that here: e13c20f

toRemove = append(toRemove, labels.Label{Name: m.Name, Value: val})
}
vals, err := lvalsFn(m.Name, not(m.Matches))
toRemove := make([]labels.Label, len(vals))
Copy link
Member

Choose a reason for hiding this comment

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

nit: we could skip this allocation if err was non-nil, same below

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Discussed offline and will leave as is. The error case is very rare here (even though that's not the contract, with current implementation it would only happen if the block is corrupted), so we'd just clutter the code with an extra err check (plus the cpu cost!) for a very rare case.

Copy link
Collaborator

@pracucci pracucci left a comment

Choose a reason for hiding this comment

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

Makes sense to me. I also agree with Peter's comments.

More often than not we retrieve label values to filter them using a
matcher later. If we pass in the matcher as a filter function, we can
avoid moving some extra values.

This shows a -5% cpu improvement when this path is hit, and no change
otherwise.

Signed-off-by: Oleg Zaytsev <mail@olegzaytsev.com>
Signed-off-by: Oleg Zaytsev <mail@olegzaytsev.com>
Instead of asking for an ad-hoc function, ask for an interface that has
just the function we need.

Signed-off-by: Oleg Zaytsev <mail@olegzaytsev.com>
@colega colega force-pushed the index-reader-label-values-matcher branch from 2372186 to e13c20f Compare November 7, 2022 14:25
@pstibrany
Copy link
Member

Makes sense to me. I also agree with Peter's comments.

My comments are non-blocking nits, feel free to ignore them.

@colega colega enabled auto-merge (squash) November 7, 2022 14:28
@colega colega merged commit a9b4b69 into main Nov 7, 2022
@colega colega deleted the index-reader-label-values-matcher branch November 7, 2022 14:39
masonmei pushed a commit to udmire/mimir that referenced this pull request Dec 16, 2022
* Add optional matcher to IndexReader.LabelValues

More often than not we retrieve label values to filter them using a
matcher later. If we pass in the matcher as a filter function, we can
avoid moving some extra values.

This shows a -5% cpu improvement when this path is hit, and no change
otherwise.

Signed-off-by: Oleg Zaytsev <mail@olegzaytsev.com>

* Update CHANGELOG.md

Signed-off-by: Oleg Zaytsev <mail@olegzaytsev.com>

* Cleaner toPostingsGroup signature

Instead of asking for an ad-hoc function, ask for an interface that has
just the function we need.

Signed-off-by: Oleg Zaytsev <mail@olegzaytsev.com>

Signed-off-by: Oleg Zaytsev <mail@olegzaytsev.com>
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.

None yet

3 participants