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

Rework iteration to avoid overflow #68

Merged
merged 8 commits into from
Oct 24, 2017
Merged

Conversation

marshallpierce
Copy link
Collaborator

Incorporating a gaggle of smaller fixes I came upon while working on this:

  • Iteration now stops when the index with the max value is reached rather than going by total count. This avoids stopping too early when total count saturates (fixes Iteration stops too early when total_count has saturated #47, and a few parts of Address remaining logic that can overflow #38). Count since last iteration is also started from 0 at each iteration point, making it less prone to overflow.
  • Linear iteration was stopping one iteration too soon in some cases due to a subtle off-by-one.
  • Quantile iteration now skips straight to quantile 1.0 if it reaches the index with the max value, just like the Java impl. See long-winded comments, and even more long-winded tests.
  • Rather than having pickers provide accessors for metadata about what was just picked, have pick() return a metadata struct with optional fields so that the timeline is clear and pickers don't need mostly useless fields to hang on to data until it can be requested 1 function call later.
  • Ported iteration tests from the old rust implementation.

Marshall Pierce and others added 6 commits October 18, 2017 17:39
…porky enough to stand on its own and data_access is a port of a Java test file anyway.
- `LinearIterator` no longer aborts one step too early in the final bucket.
- `PickyIterator` now returns metadata when it picks. This allows `IterationValue` to provide better data about the current iteration progress without introducing a separate stage in the `PickyIterator` lifecycle to query about what was just picked.
- count since last iteration is now reset every iteration, making it less prone to overflow.
- end-of-histogram is detected by comparing with max nonzero index, not total count, which avoids overflow.
- Supply count at current index to `pick()` since we already have that available
- Quantile iterator won't get stuck asymptotically chasing quantile 1.0_f64
- More tests
self.value
/// The value iterated to. Some iterators provide a specific value inside the bucket, while
/// others just use the highest value in the bucket.
pub fn value_iterated_to(&self) -> u64 {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Maybe this rename-for-clarity is not worth the compatibility concern?

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 it's fine.


// make sure we don't add this index again
self.fresh = false;
}
}

// figure out if picker thinks we should yield this value
if self.picker.pick(self.current_index, self.total_count_to_index) {
let val = self.current();
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 felt it was easier to reason at-a-glance about when the iterator's fields were used with this function inlined, since it matters that you read things like count_since_last_iteration before resetting a few lines below.


/// An iterator that will yield at quantile steps through the histogram's value range.
pub struct Iter<'a, T: 'a + Counter> {
hist: &'a Histogram<T>,
ticks_per_half_distance: u32,
quantile_to_iterate_to: f64,
quantile_just_picked: f64
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

this is what returning Option<PickMetadata> lets us avoid

return None;
}

// Because there are effectively two quantiles in play (the quantile of the value for the
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

comments per LoC might be getting a little out of hand in this struct, but I like to beat historically confusing logic into submission with overwhelming documentation

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 no objection to this comment. Seems well-placed and useful.

@@ -542,365 +539,3 @@ fn total_count_exceeds_bucket_type() {

assert_eq!(400, h.count());
}

#[test]
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

quantile tests moved to their own file


// TODO count starting at 0 each time we emit a value to be less prone to overflow
self.prev_total_count = self.total_count_to_index;
if let Some(metadata) = self.picker.pick(self.current_index, self.total_count_to_index, self.count_at_index) {
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 rustfmt might complain about this line.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Good point. I haven't been rustfmt-ing but I'm happy to adopt it. I don't even really care if its formatting is suboptimal for some aesthetic; consistency is good. I'll format the iterator files.

@marshallpierce
Copy link
Collaborator Author

@algermissen here's how the quantile iteration example looks now:

jot 10000 0 10000000000 | cg run --example cli -- serialize -c | cg run --example cli -- iter-quantiles -t 5
       Value          QuantileValue      QuantileIteration TotalCount 1/(1-Quantile)

           0 0.00010000000000000000 0.00000000000000000000          1           1.00
   999292927 0.10000000000000000555 0.10000000000000000555       1000           1.11
  1999634431 0.20000000000000001110 0.20000000000000001110       2000           1.25
  3001024511 0.30009999999999997788 0.30000000000000004441       3001           1.43
...
  9999220735 0.99990000000000001101 0.99985351562500002220       9999        6826.67
  9999220735 0.99990000000000001101 0.99987792968750000000       9999        8192.00
  9999220735 0.99990000000000001101 0.99989013671875004441       9999        9102.22
 10007609343 1.00000000000000000000 0.99990234375000008882      10000       10240.00
 10007609343 1.00000000000000000000 1.00000000000000000000      10000              ∞
#[Mean       = 5000000916.51, StdDeviation   = 2887040392.98]
#[Max        =  10007609343, Total count    =        10000]
#[Buckets    =           54, SubBuckets     =        56320]

The 1/(1-quantile) column is now using the quantile iterated to, not the quantile of the value of the current bucket, so there's only one row with ∞. Also, quantile iteration now skips all intermediate steps to 1.0 once it reaches the last bucket. (Small proviso on that: if total count saturates, it doesn't do the skipping thing. Could be fixed with more fiddling, but this PR was getting complex enough, and probably nobody will ever complain about that...)

@jonhoo
Copy link
Collaborator

jonhoo commented Oct 24, 2017

This is great! I also think the long comment is useful (not to mention the many extra tests).

See my one comment about rustfmt, but apart from that, lgtm.

@marshallpierce marshallpierce merged commit 2758171 into master Oct 24, 2017
jonhoo added a commit that referenced this pull request Oct 24, 2017
This release has a couple of backwards-incompatible changes:

 - the old `len()` is now `distinct_values()`
 - the new `len()` is the old `count()` (which is deprecated)
 - `IterationValue::value` became `value_iterated_to`

Some other API changes:

 - iterator values gained `quantile_iterated_to()`
 - `Histogram` gained `is_empty()`

Behind the scenes:

 - #67 and #68 landed a number of fixes to iterators such that the
   produced values are more correct and sensible.
 - errors were moved into their own module.
@algermissen
Copy link

@marshallpierce Thanks a lot, the iteration logic looks definitely fine now. The double line at end is gone for my cases.

One nit: The CLI output adds a column which breaks the format named 'hgrm' by the original hdrhistogram and CLI output cannot be loaded by the original plot HTML pages. No big deal, just wanted to give that feedback.

@marshallpierce
Copy link
Collaborator Author

marshallpierce commented Oct 26, 2017

@algermissen yeah, I'm a little conflicted on that.

Anti-extra-column:

  • It's lame to break other tools by tweaking formats.

Pro-extra-column:

  • The extended format is a little more comprehensible since it shows why it's possible to have a quantile change without the value changing
  • We shouldn't be parsing a fragile text format for machine processing; we should use tools like https://hdrhistogram.github.io/HdrHistogramJSDemo/plotFiles.html that operate on the serialized histogram

Overall I'm weakly in favor of keeping the output the way it is (human oriented) but I could be persuaded.

@algermissen
Copy link

@marshallpierce I agree with you - I am only building a proof of concept throw-away tool, so I am looking for the least amount of work. Otherwise I'd never use the text format.

As I am fine with copy/pasting your loop and removing the col by hand, no need for me to pursuade you in dropping the col :-)

Keep it as it is - it's better - my 2ct

@marshallpierce marshallpierce deleted the iter-recorded-total-count branch November 1, 2017 01:44
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.

Iteration stops too early when total_count has saturated
3 participants