| 
									
										
										
										
											2022-09-29 22:46:48 +08:00
										 |  |  | // Copyright 2022 The Prometheus Authors
 | 
					
						
							|  |  |  | // Licensed under the Apache License, Version 2.0 (the "License");
 | 
					
						
							|  |  |  | // you may not use this file except in compliance with the License.
 | 
					
						
							|  |  |  | // You may obtain a copy of the License at
 | 
					
						
							|  |  |  | //
 | 
					
						
							|  |  |  | // http://www.apache.org/licenses/LICENSE-2.0
 | 
					
						
							|  |  |  | //
 | 
					
						
							|  |  |  | // Unless required by applicable law or agreed to in writing, software
 | 
					
						
							|  |  |  | // distributed under the License is distributed on an "AS IS" BASIS,
 | 
					
						
							|  |  |  | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
					
						
							|  |  |  | // See the License for the specific language governing permissions and
 | 
					
						
							|  |  |  | // limitations under the License.
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | package jsonutil | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | import ( | 
					
						
							|  |  |  | 	"math" | 
					
						
							|  |  |  | 	"strconv" | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	jsoniter "github.com/json-iterator/go" | 
					
						
							| 
									
										
											  
											
												promql: Separate `Point` into `FPoint` and `HPoint`
In other words: Instead of having a “polymorphous” `Point` that can
either contain a float value or a histogram value, use an `FPoint` for
floats and an `HPoint` for histograms.
This seemingly small change has a _lot_ of repercussions throughout
the codebase.
The idea here is to avoid the increase in size of `Point` arrays that
happened after native histograms had been added.
The higher-level data structures (`Sample`, `Series`, etc.) are still
“polymorphous”. The same idea could be applied to them, but at each
step the trade-offs needed to be evaluated.
The idea with this change is to do the minimum necessary to get back
to pre-histogram performance for functions that do not touch
histograms. Here are comparisons for the `changes` function. The test
data doesn't include histograms yet. Ideally, there would be no change
in the benchmark result at all.
First runtime v2.39 compared to directly prior to this commit:
```
name                                                  old time/op    new time/op    delta
RangeQuery/expr=changes(a_one[1d]),steps=1-16            391µs ± 2%     542µs ± 1%  +38.58%  (p=0.000 n=9+8)
RangeQuery/expr=changes(a_one[1d]),steps=10-16           452µs ± 2%     617µs ± 2%  +36.48%  (p=0.000 n=10+10)
RangeQuery/expr=changes(a_one[1d]),steps=100-16         1.12ms ± 1%    1.36ms ± 2%  +21.58%  (p=0.000 n=8+10)
RangeQuery/expr=changes(a_one[1d]),steps=1000-16        7.83ms ± 1%    8.94ms ± 1%  +14.21%  (p=0.000 n=10+10)
RangeQuery/expr=changes(a_ten[1d]),steps=1-16           2.98ms ± 0%    3.30ms ± 1%  +10.67%  (p=0.000 n=9+10)
RangeQuery/expr=changes(a_ten[1d]),steps=10-16          3.66ms ± 1%    4.10ms ± 1%  +11.82%  (p=0.000 n=10+10)
RangeQuery/expr=changes(a_ten[1d]),steps=100-16         10.5ms ± 0%    11.8ms ± 1%  +12.50%  (p=0.000 n=8+10)
RangeQuery/expr=changes(a_ten[1d]),steps=1000-16        77.6ms ± 1%    87.4ms ± 1%  +12.63%  (p=0.000 n=9+9)
RangeQuery/expr=changes(a_hundred[1d]),steps=1-16       30.4ms ± 2%    32.8ms ± 1%   +8.01%  (p=0.000 n=10+10)
RangeQuery/expr=changes(a_hundred[1d]),steps=10-16      37.1ms ± 2%    40.6ms ± 2%   +9.64%  (p=0.000 n=10+10)
RangeQuery/expr=changes(a_hundred[1d]),steps=100-16      105ms ± 1%     117ms ± 1%  +11.69%  (p=0.000 n=10+10)
RangeQuery/expr=changes(a_hundred[1d]),steps=1000-16     783ms ± 3%     876ms ± 1%  +11.83%  (p=0.000 n=9+10)
```
And then runtime v2.39 compared to after this commit:
```
name                                                  old time/op    new time/op    delta
RangeQuery/expr=changes(a_one[1d]),steps=1-16            391µs ± 2%     547µs ± 1%  +39.84%  (p=0.000 n=9+8)
RangeQuery/expr=changes(a_one[1d]),steps=10-16           452µs ± 2%     616µs ± 2%  +36.15%  (p=0.000 n=10+10)
RangeQuery/expr=changes(a_one[1d]),steps=100-16         1.12ms ± 1%    1.26ms ± 1%  +12.20%  (p=0.000 n=8+10)
RangeQuery/expr=changes(a_one[1d]),steps=1000-16        7.83ms ± 1%    7.95ms ± 1%   +1.59%  (p=0.000 n=10+8)
RangeQuery/expr=changes(a_ten[1d]),steps=1-16           2.98ms ± 0%    3.38ms ± 2%  +13.49%  (p=0.000 n=9+10)
RangeQuery/expr=changes(a_ten[1d]),steps=10-16          3.66ms ± 1%    4.02ms ± 1%   +9.80%  (p=0.000 n=10+9)
RangeQuery/expr=changes(a_ten[1d]),steps=100-16         10.5ms ± 0%    10.8ms ± 1%   +3.08%  (p=0.000 n=8+10)
RangeQuery/expr=changes(a_ten[1d]),steps=1000-16        77.6ms ± 1%    78.1ms ± 1%   +0.58%  (p=0.035 n=9+10)
RangeQuery/expr=changes(a_hundred[1d]),steps=1-16       30.4ms ± 2%    33.5ms ± 4%  +10.18%  (p=0.000 n=10+10)
RangeQuery/expr=changes(a_hundred[1d]),steps=10-16      37.1ms ± 2%    40.0ms ± 1%   +7.98%  (p=0.000 n=10+10)
RangeQuery/expr=changes(a_hundred[1d]),steps=100-16      105ms ± 1%     107ms ± 1%   +1.92%  (p=0.000 n=10+10)
RangeQuery/expr=changes(a_hundred[1d]),steps=1000-16     783ms ± 3%     775ms ± 1%   -1.02%  (p=0.019 n=9+9)
```
In summary, the runtime doesn't really improve with this change for
queries with just a few steps. For queries with many steps, this
commit essentially reinstates the old performance. This is good
because the many-step queries are the one that matter most (longest
absolute runtime).
In terms of allocations, though, this commit doesn't make a dent at
all (numbers not shown). The reason is that most of the allocations
happen in the sampleRingIterator (in the storage package), which has
to be addressed in a separate commit.
Signed-off-by: beorn7 <beorn@grafana.com>
											
										 
											2022-10-28 22:58:40 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	"github.com/prometheus/prometheus/model/histogram" | 
					
						
							| 
									
										
										
										
											2022-09-29 22:46:48 +08:00
										 |  |  | ) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // MarshalTimestamp marshals a point timestamp using the passed jsoniter stream.
 | 
					
						
							|  |  |  | func MarshalTimestamp(t int64, stream *jsoniter.Stream) { | 
					
						
							|  |  |  | 	// Write out the timestamp as a float divided by 1000.
 | 
					
						
							|  |  |  | 	// This is ~3x faster than converting to a float.
 | 
					
						
							|  |  |  | 	if t < 0 { | 
					
						
							|  |  |  | 		stream.WriteRaw(`-`) | 
					
						
							|  |  |  | 		t = -t | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	stream.WriteInt64(t / 1000) | 
					
						
							|  |  |  | 	fraction := t % 1000 | 
					
						
							|  |  |  | 	if fraction != 0 { | 
					
						
							|  |  |  | 		stream.WriteRaw(`.`) | 
					
						
							|  |  |  | 		if fraction < 100 { | 
					
						
							|  |  |  | 			stream.WriteRaw(`0`) | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		if fraction < 10 { | 
					
						
							|  |  |  | 			stream.WriteRaw(`0`) | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		stream.WriteInt64(fraction) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
											  
											
												promql: Separate `Point` into `FPoint` and `HPoint`
In other words: Instead of having a “polymorphous” `Point` that can
either contain a float value or a histogram value, use an `FPoint` for
floats and an `HPoint` for histograms.
This seemingly small change has a _lot_ of repercussions throughout
the codebase.
The idea here is to avoid the increase in size of `Point` arrays that
happened after native histograms had been added.
The higher-level data structures (`Sample`, `Series`, etc.) are still
“polymorphous”. The same idea could be applied to them, but at each
step the trade-offs needed to be evaluated.
The idea with this change is to do the minimum necessary to get back
to pre-histogram performance for functions that do not touch
histograms. Here are comparisons for the `changes` function. The test
data doesn't include histograms yet. Ideally, there would be no change
in the benchmark result at all.
First runtime v2.39 compared to directly prior to this commit:
```
name                                                  old time/op    new time/op    delta
RangeQuery/expr=changes(a_one[1d]),steps=1-16            391µs ± 2%     542µs ± 1%  +38.58%  (p=0.000 n=9+8)
RangeQuery/expr=changes(a_one[1d]),steps=10-16           452µs ± 2%     617µs ± 2%  +36.48%  (p=0.000 n=10+10)
RangeQuery/expr=changes(a_one[1d]),steps=100-16         1.12ms ± 1%    1.36ms ± 2%  +21.58%  (p=0.000 n=8+10)
RangeQuery/expr=changes(a_one[1d]),steps=1000-16        7.83ms ± 1%    8.94ms ± 1%  +14.21%  (p=0.000 n=10+10)
RangeQuery/expr=changes(a_ten[1d]),steps=1-16           2.98ms ± 0%    3.30ms ± 1%  +10.67%  (p=0.000 n=9+10)
RangeQuery/expr=changes(a_ten[1d]),steps=10-16          3.66ms ± 1%    4.10ms ± 1%  +11.82%  (p=0.000 n=10+10)
RangeQuery/expr=changes(a_ten[1d]),steps=100-16         10.5ms ± 0%    11.8ms ± 1%  +12.50%  (p=0.000 n=8+10)
RangeQuery/expr=changes(a_ten[1d]),steps=1000-16        77.6ms ± 1%    87.4ms ± 1%  +12.63%  (p=0.000 n=9+9)
RangeQuery/expr=changes(a_hundred[1d]),steps=1-16       30.4ms ± 2%    32.8ms ± 1%   +8.01%  (p=0.000 n=10+10)
RangeQuery/expr=changes(a_hundred[1d]),steps=10-16      37.1ms ± 2%    40.6ms ± 2%   +9.64%  (p=0.000 n=10+10)
RangeQuery/expr=changes(a_hundred[1d]),steps=100-16      105ms ± 1%     117ms ± 1%  +11.69%  (p=0.000 n=10+10)
RangeQuery/expr=changes(a_hundred[1d]),steps=1000-16     783ms ± 3%     876ms ± 1%  +11.83%  (p=0.000 n=9+10)
```
And then runtime v2.39 compared to after this commit:
```
name                                                  old time/op    new time/op    delta
RangeQuery/expr=changes(a_one[1d]),steps=1-16            391µs ± 2%     547µs ± 1%  +39.84%  (p=0.000 n=9+8)
RangeQuery/expr=changes(a_one[1d]),steps=10-16           452µs ± 2%     616µs ± 2%  +36.15%  (p=0.000 n=10+10)
RangeQuery/expr=changes(a_one[1d]),steps=100-16         1.12ms ± 1%    1.26ms ± 1%  +12.20%  (p=0.000 n=8+10)
RangeQuery/expr=changes(a_one[1d]),steps=1000-16        7.83ms ± 1%    7.95ms ± 1%   +1.59%  (p=0.000 n=10+8)
RangeQuery/expr=changes(a_ten[1d]),steps=1-16           2.98ms ± 0%    3.38ms ± 2%  +13.49%  (p=0.000 n=9+10)
RangeQuery/expr=changes(a_ten[1d]),steps=10-16          3.66ms ± 1%    4.02ms ± 1%   +9.80%  (p=0.000 n=10+9)
RangeQuery/expr=changes(a_ten[1d]),steps=100-16         10.5ms ± 0%    10.8ms ± 1%   +3.08%  (p=0.000 n=8+10)
RangeQuery/expr=changes(a_ten[1d]),steps=1000-16        77.6ms ± 1%    78.1ms ± 1%   +0.58%  (p=0.035 n=9+10)
RangeQuery/expr=changes(a_hundred[1d]),steps=1-16       30.4ms ± 2%    33.5ms ± 4%  +10.18%  (p=0.000 n=10+10)
RangeQuery/expr=changes(a_hundred[1d]),steps=10-16      37.1ms ± 2%    40.0ms ± 1%   +7.98%  (p=0.000 n=10+10)
RangeQuery/expr=changes(a_hundred[1d]),steps=100-16      105ms ± 1%     107ms ± 1%   +1.92%  (p=0.000 n=10+10)
RangeQuery/expr=changes(a_hundred[1d]),steps=1000-16     783ms ± 3%     775ms ± 1%   -1.02%  (p=0.019 n=9+9)
```
In summary, the runtime doesn't really improve with this change for
queries with just a few steps. For queries with many steps, this
commit essentially reinstates the old performance. This is good
because the many-step queries are the one that matter most (longest
absolute runtime).
In terms of allocations, though, this commit doesn't make a dent at
all (numbers not shown). The reason is that most of the allocations
happen in the sampleRingIterator (in the storage package), which has
to be addressed in a separate commit.
Signed-off-by: beorn7 <beorn@grafana.com>
											
										 
											2022-10-28 22:58:40 +08:00
										 |  |  | // MarshalFloat marshals a float value using the passed jsoniter stream.
 | 
					
						
							| 
									
										
										
										
											2023-03-31 01:50:13 +08:00
										 |  |  | func MarshalFloat(f float64, stream *jsoniter.Stream) { | 
					
						
							| 
									
										
										
										
											2022-09-29 22:46:48 +08:00
										 |  |  | 	stream.WriteRaw(`"`) | 
					
						
							|  |  |  | 	// Taken from https://github.com/json-iterator/go/blob/master/stream_float.go#L71 as a workaround
 | 
					
						
							|  |  |  | 	// to https://github.com/json-iterator/go/issues/365 (jsoniter, to follow json standard, doesn't allow inf/nan).
 | 
					
						
							|  |  |  | 	buf := stream.Buffer() | 
					
						
							| 
									
										
										
										
											2023-03-31 01:50:13 +08:00
										 |  |  | 	abs := math.Abs(f) | 
					
						
							| 
									
										
										
										
											2022-09-29 22:46:48 +08:00
										 |  |  | 	fmt := byte('f') | 
					
						
							|  |  |  | 	// Note: Must use float32 comparisons for underlying float32 value to get precise cutoffs right.
 | 
					
						
							|  |  |  | 	if abs != 0 { | 
					
						
							|  |  |  | 		if abs < 1e-6 || abs >= 1e21 { | 
					
						
							|  |  |  | 			fmt = 'e' | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							| 
									
										
										
										
											2023-03-31 01:50:13 +08:00
										 |  |  | 	buf = strconv.AppendFloat(buf, f, fmt, -1, 64) | 
					
						
							| 
									
										
										
										
											2022-09-29 22:46:48 +08:00
										 |  |  | 	stream.SetBuffer(buf) | 
					
						
							|  |  |  | 	stream.WriteRaw(`"`) | 
					
						
							|  |  |  | } | 
					
						
							| 
									
										
											  
											
												promql: Separate `Point` into `FPoint` and `HPoint`
In other words: Instead of having a “polymorphous” `Point` that can
either contain a float value or a histogram value, use an `FPoint` for
floats and an `HPoint` for histograms.
This seemingly small change has a _lot_ of repercussions throughout
the codebase.
The idea here is to avoid the increase in size of `Point` arrays that
happened after native histograms had been added.
The higher-level data structures (`Sample`, `Series`, etc.) are still
“polymorphous”. The same idea could be applied to them, but at each
step the trade-offs needed to be evaluated.
The idea with this change is to do the minimum necessary to get back
to pre-histogram performance for functions that do not touch
histograms. Here are comparisons for the `changes` function. The test
data doesn't include histograms yet. Ideally, there would be no change
in the benchmark result at all.
First runtime v2.39 compared to directly prior to this commit:
```
name                                                  old time/op    new time/op    delta
RangeQuery/expr=changes(a_one[1d]),steps=1-16            391µs ± 2%     542µs ± 1%  +38.58%  (p=0.000 n=9+8)
RangeQuery/expr=changes(a_one[1d]),steps=10-16           452µs ± 2%     617µs ± 2%  +36.48%  (p=0.000 n=10+10)
RangeQuery/expr=changes(a_one[1d]),steps=100-16         1.12ms ± 1%    1.36ms ± 2%  +21.58%  (p=0.000 n=8+10)
RangeQuery/expr=changes(a_one[1d]),steps=1000-16        7.83ms ± 1%    8.94ms ± 1%  +14.21%  (p=0.000 n=10+10)
RangeQuery/expr=changes(a_ten[1d]),steps=1-16           2.98ms ± 0%    3.30ms ± 1%  +10.67%  (p=0.000 n=9+10)
RangeQuery/expr=changes(a_ten[1d]),steps=10-16          3.66ms ± 1%    4.10ms ± 1%  +11.82%  (p=0.000 n=10+10)
RangeQuery/expr=changes(a_ten[1d]),steps=100-16         10.5ms ± 0%    11.8ms ± 1%  +12.50%  (p=0.000 n=8+10)
RangeQuery/expr=changes(a_ten[1d]),steps=1000-16        77.6ms ± 1%    87.4ms ± 1%  +12.63%  (p=0.000 n=9+9)
RangeQuery/expr=changes(a_hundred[1d]),steps=1-16       30.4ms ± 2%    32.8ms ± 1%   +8.01%  (p=0.000 n=10+10)
RangeQuery/expr=changes(a_hundred[1d]),steps=10-16      37.1ms ± 2%    40.6ms ± 2%   +9.64%  (p=0.000 n=10+10)
RangeQuery/expr=changes(a_hundred[1d]),steps=100-16      105ms ± 1%     117ms ± 1%  +11.69%  (p=0.000 n=10+10)
RangeQuery/expr=changes(a_hundred[1d]),steps=1000-16     783ms ± 3%     876ms ± 1%  +11.83%  (p=0.000 n=9+10)
```
And then runtime v2.39 compared to after this commit:
```
name                                                  old time/op    new time/op    delta
RangeQuery/expr=changes(a_one[1d]),steps=1-16            391µs ± 2%     547µs ± 1%  +39.84%  (p=0.000 n=9+8)
RangeQuery/expr=changes(a_one[1d]),steps=10-16           452µs ± 2%     616µs ± 2%  +36.15%  (p=0.000 n=10+10)
RangeQuery/expr=changes(a_one[1d]),steps=100-16         1.12ms ± 1%    1.26ms ± 1%  +12.20%  (p=0.000 n=8+10)
RangeQuery/expr=changes(a_one[1d]),steps=1000-16        7.83ms ± 1%    7.95ms ± 1%   +1.59%  (p=0.000 n=10+8)
RangeQuery/expr=changes(a_ten[1d]),steps=1-16           2.98ms ± 0%    3.38ms ± 2%  +13.49%  (p=0.000 n=9+10)
RangeQuery/expr=changes(a_ten[1d]),steps=10-16          3.66ms ± 1%    4.02ms ± 1%   +9.80%  (p=0.000 n=10+9)
RangeQuery/expr=changes(a_ten[1d]),steps=100-16         10.5ms ± 0%    10.8ms ± 1%   +3.08%  (p=0.000 n=8+10)
RangeQuery/expr=changes(a_ten[1d]),steps=1000-16        77.6ms ± 1%    78.1ms ± 1%   +0.58%  (p=0.035 n=9+10)
RangeQuery/expr=changes(a_hundred[1d]),steps=1-16       30.4ms ± 2%    33.5ms ± 4%  +10.18%  (p=0.000 n=10+10)
RangeQuery/expr=changes(a_hundred[1d]),steps=10-16      37.1ms ± 2%    40.0ms ± 1%   +7.98%  (p=0.000 n=10+10)
RangeQuery/expr=changes(a_hundred[1d]),steps=100-16      105ms ± 1%     107ms ± 1%   +1.92%  (p=0.000 n=10+10)
RangeQuery/expr=changes(a_hundred[1d]),steps=1000-16     783ms ± 3%     775ms ± 1%   -1.02%  (p=0.019 n=9+9)
```
In summary, the runtime doesn't really improve with this change for
queries with just a few steps. For queries with many steps, this
commit essentially reinstates the old performance. This is good
because the many-step queries are the one that matter most (longest
absolute runtime).
In terms of allocations, though, this commit doesn't make a dent at
all (numbers not shown). The reason is that most of the allocations
happen in the sampleRingIterator (in the storage package), which has
to be addressed in a separate commit.
Signed-off-by: beorn7 <beorn@grafana.com>
											
										 
											2022-10-28 22:58:40 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | // MarshalHistogram marshals a histogram value using the passed jsoniter stream.
 | 
					
						
							|  |  |  | // It writes something like:
 | 
					
						
							|  |  |  | //
 | 
					
						
							|  |  |  | //	{
 | 
					
						
							|  |  |  | //	    "count": "42",
 | 
					
						
							|  |  |  | //	    "sum": "34593.34",
 | 
					
						
							|  |  |  | //	    "buckets": [
 | 
					
						
							|  |  |  | //	      [ 3, "-0.25", "0.25", "3"],
 | 
					
						
							|  |  |  | //	      [ 0, "0.25", "0.5", "12"],
 | 
					
						
							|  |  |  | //	      [ 0, "0.5", "1", "21"],
 | 
					
						
							|  |  |  | //	      [ 0, "2", "4", "6"]
 | 
					
						
							|  |  |  | //	    ]
 | 
					
						
							|  |  |  | //	}
 | 
					
						
							|  |  |  | //
 | 
					
						
							|  |  |  | // The 1st element in each bucket array determines if the boundaries are
 | 
					
						
							|  |  |  | // inclusive (AKA closed) or exclusive (AKA open):
 | 
					
						
							|  |  |  | //
 | 
					
						
							|  |  |  | //	0: lower exclusive, upper inclusive
 | 
					
						
							|  |  |  | //	1: lower inclusive, upper exclusive
 | 
					
						
							|  |  |  | //	2: both exclusive
 | 
					
						
							|  |  |  | //	3: both inclusive
 | 
					
						
							|  |  |  | //
 | 
					
						
							|  |  |  | // The 2nd and 3rd elements are the lower and upper boundary. The 4th element is
 | 
					
						
							|  |  |  | // the bucket count.
 | 
					
						
							|  |  |  | func MarshalHistogram(h *histogram.FloatHistogram, stream *jsoniter.Stream) { | 
					
						
							|  |  |  | 	stream.WriteObjectStart() | 
					
						
							|  |  |  | 	stream.WriteObjectField(`count`) | 
					
						
							|  |  |  | 	MarshalFloat(h.Count, stream) | 
					
						
							|  |  |  | 	stream.WriteMore() | 
					
						
							|  |  |  | 	stream.WriteObjectField(`sum`) | 
					
						
							|  |  |  | 	MarshalFloat(h.Sum, stream) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	bucketFound := false | 
					
						
							|  |  |  | 	it := h.AllBucketIterator() | 
					
						
							|  |  |  | 	for it.Next() { | 
					
						
							|  |  |  | 		bucket := it.At() | 
					
						
							|  |  |  | 		if bucket.Count == 0 { | 
					
						
							|  |  |  | 			continue // No need to expose empty buckets in JSON.
 | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		stream.WriteMore() | 
					
						
							|  |  |  | 		if !bucketFound { | 
					
						
							|  |  |  | 			stream.WriteObjectField(`buckets`) | 
					
						
							|  |  |  | 			stream.WriteArrayStart() | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		bucketFound = true | 
					
						
							|  |  |  | 		boundaries := 2 // Exclusive on both sides AKA open interval.
 | 
					
						
							|  |  |  | 		if bucket.LowerInclusive { | 
					
						
							|  |  |  | 			if bucket.UpperInclusive { | 
					
						
							|  |  |  | 				boundaries = 3 // Inclusive on both sides AKA closed interval.
 | 
					
						
							|  |  |  | 			} else { | 
					
						
							|  |  |  | 				boundaries = 1 // Inclusive only on lower end AKA right open.
 | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 		} else { | 
					
						
							|  |  |  | 			if bucket.UpperInclusive { | 
					
						
							|  |  |  | 				boundaries = 0 // Inclusive only on upper end AKA left open.
 | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		stream.WriteArrayStart() | 
					
						
							|  |  |  | 		stream.WriteInt(boundaries) | 
					
						
							|  |  |  | 		stream.WriteMore() | 
					
						
							|  |  |  | 		MarshalFloat(bucket.Lower, stream) | 
					
						
							|  |  |  | 		stream.WriteMore() | 
					
						
							|  |  |  | 		MarshalFloat(bucket.Upper, stream) | 
					
						
							|  |  |  | 		stream.WriteMore() | 
					
						
							|  |  |  | 		MarshalFloat(bucket.Count, stream) | 
					
						
							|  |  |  | 		stream.WriteArrayEnd() | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	if bucketFound { | 
					
						
							|  |  |  | 		stream.WriteArrayEnd() | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	stream.WriteObjectEnd() | 
					
						
							|  |  |  | } |