From d3afd10340b9359783634bc5a10ac85af345e4d6 Mon Sep 17 00:00:00 2001 From: Taylor Blau Date: Thu, 4 Jan 2018 14:12:02 -0800 Subject: [PATCH] tools/humanize: add 'FormatByteRate' to format transfer speed --- tools/humanize/humanize.go | 37 +++++++++++++++++++++++++++ tools/humanize/humanize_test.go | 44 +++++++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+) diff --git a/tools/humanize/humanize.go b/tools/humanize/humanize.go index 57bdf324..2d6023ec 100644 --- a/tools/humanize/humanize.go +++ b/tools/humanize/humanize.go @@ -5,6 +5,7 @@ import ( "math" "strconv" "strings" + "time" "unicode" "github.com/git-lfs/git-lfs/errors" @@ -23,6 +24,10 @@ const ( Gigabyte = 1000 * Megabyte Terabyte = 1000 * Gigabyte Petabyte = 1000 * Terabyte + + // eps is the machine epsilon, or a 64-bit floating point value + // reasonably close to zero. + eps float64 = 7.0/3 - 4.0/3 - 1.0 ) var bytesTable = map[string]uint64{ @@ -126,6 +131,38 @@ func FormatBytesUnit(s, u uint64) string { return fmt.Sprintf(format, rounded) } +// FormatByteRate outputs the given rate of transfer "r" as the quotient of "s" +// (the number of bytes transferred) over "d" (the duration of time that those +// bytes were transferred in). +// +// It displays the output as a quantity of a "per-unit-time" unit (i.e., B/s, +// MiB/s) in the most representative fashion possible, as above. +func FormatByteRate(s uint64, d time.Duration) string { + // e is the index of the most representative unit of storage. + var e float64 + + // f is the floating-point equivalent of "s", so as to avoid more + // conversions than necessary. + f := float64(s) + + if f != 0 { + f = f / d.Seconds() + e = math.Floor(log(f, 1000)) + if e <= eps { + // The result of math.Floor(log(r, 1000)) can be + // "close-enough" to zero that it should be effectively + // considered zero. + e = 0 + } + } + + unit := uint64(math.Pow(1000, e)) + suffix := sizes[int(e)] + + return fmt.Sprintf("%s %s/s", + FormatBytesUnit(uint64(math.Ceil(f)), unit), suffix) +} + // log takes the log base "b" of "n" (\log_b{n}) func log(n, b float64) float64 { return math.Log(n) / math.Log(b) diff --git a/tools/humanize/humanize_test.go b/tools/humanize/humanize_test.go index 88a8b52e..c151254e 100644 --- a/tools/humanize/humanize_test.go +++ b/tools/humanize/humanize_test.go @@ -3,6 +3,7 @@ package humanize_test import ( "math" "testing" + "time" "github.com/git-lfs/git-lfs/tools/humanize" "github.com/stretchr/testify/assert" @@ -59,6 +60,16 @@ func (c *FormatBytesUnitTestCase) Assert(t *testing.T) { assert.Equal(t, c.Expected, humanize.FormatBytesUnit(c.Given, c.Unit)) } +type FormatByteRateTestCase struct { + Given uint64 + Over time.Duration + Expected string +} + +func (c *FormatByteRateTestCase) Assert(t *testing.T) { + assert.Equal(t, c.Expected, humanize.FormatByteRate(c.Given, c.Over)) +} + func TestParseBytes(t *testing.T) { for desc, c := range map[string]*ParseBytesTestCase{ "parse byte (zero, empty)": {"", uint64(0), nil}, @@ -234,3 +245,36 @@ func TestFormatBytesUnit(t *testing.T) { t.Run(desc, c.Assert) } } + +func TestFormateByteRate(t *testing.T) { + for desc, c := range map[string]*FormatByteRateTestCase{ + "format bytes": {uint64(1 * math.Pow(10, 0)), time.Second, "1 B/s"}, + "format kilobytes": {uint64(1 * math.Pow(10, 3)), time.Second, "1.0 KB/s"}, + "format megabytes": {uint64(1 * math.Pow(10, 6)), time.Second, "1.0 MB/s"}, + "format gigabytes": {uint64(1 * math.Pow(10, 9)), time.Second, "1.0 GB/s"}, + "format petabytes": {uint64(1 * math.Pow(10, 12)), time.Second, "1.0 TB/s"}, + "format terabytes": {uint64(1 * math.Pow(10, 15)), time.Second, "1.0 PB/s"}, + + "format kilobytes under": {uint64(1.49 * math.Pow(10, 3)), time.Second, "1.5 KB/s"}, + "format megabytes under": {uint64(1.49 * math.Pow(10, 6)), time.Second, "1.5 MB/s"}, + "format gigabytes under": {uint64(1.49 * math.Pow(10, 9)), time.Second, "1.5 GB/s"}, + "format petabytes under": {uint64(1.49 * math.Pow(10, 12)), time.Second, "1.5 TB/s"}, + "format terabytes under": {uint64(1.49 * math.Pow(10, 15)), time.Second, "1.5 PB/s"}, + + "format kilobytes over": {uint64(1.51 * math.Pow(10, 3)), time.Second, "1.5 KB/s"}, + "format megabytes over": {uint64(1.51 * math.Pow(10, 6)), time.Second, "1.5 MB/s"}, + "format gigabytes over": {uint64(1.51 * math.Pow(10, 9)), time.Second, "1.5 GB/s"}, + "format petabytes over": {uint64(1.51 * math.Pow(10, 12)), time.Second, "1.5 TB/s"}, + "format terabytes over": {uint64(1.51 * math.Pow(10, 15)), time.Second, "1.5 PB/s"}, + + "format kilobytes exact": {uint64(1.3 * math.Pow(10, 3)), time.Second, "1.3 KB/s"}, + "format megabytes exact": {uint64(1.3 * math.Pow(10, 6)), time.Second, "1.3 MB/s"}, + "format gigabytes exact": {uint64(1.3 * math.Pow(10, 9)), time.Second, "1.3 GB/s"}, + "format petabytes exact": {uint64(1.3 * math.Pow(10, 12)), time.Second, "1.3 TB/s"}, + "format terabytes exact": {uint64(1.3 * math.Pow(10, 15)), time.Second, "1.3 PB/s"}, + + "format bytes (non-second)": {uint64(10 * math.Pow(10, 0)), 2 * time.Second, "5 B/s"}, + } { + t.Run(desc, c.Assert) + } +}