Browse Source

Merge pull request #2784 from laytan/fmt-memory-sizes

Add formatting of bytes into the best unit of measurement
gingerBill 1 year ago
parent
commit
6e49b1cad7
7 changed files with 133 additions and 2 deletions
  1. 3 0
      core/fmt/doc.odin
  2. 61 0
      core/fmt/fmt.odin
  3. 1 1
      core/mem/doc.odin
  4. 2 0
      core/mem/mem.odin
  5. 2 0
      core/runtime/core.odin
  6. 5 1
      tests/core/Makefile
  7. 59 0
      tests/core/fmt/test_core_fmt.odin

+ 3 - 0
core/fmt/doc.odin

@@ -35,6 +35,8 @@ Floating-point, complex numbers, and quaternions:
 	%F    synonym for %f
 	%h    hexadecimal (lower-case) representation with 0h prefix (0h01234abcd)
 	%H    hexadecimal (upper-case) representation with 0H prefix (0h01234ABCD)
+	%m    number of bytes in the best unit of measurement, e.g. 123.45mib
+	%M    number of bytes in the best unit of measurement, e.g. 123.45MiB
 String and slice of bytes
 	%s    the uninterpreted bytes of the string or slice
 	%q    a double-quoted string safely escaped with Odin syntax
@@ -85,6 +87,7 @@ Other flags:
 	               add leading 0z for dozenal (%#z)
 	               add leading 0x or 0X for hexadecimal (%#x or %#X)
 	               remove leading 0x for %p (%#p)
+	               add a space between bytes and the unit of measurement (%#m or %#M)
 	' '    (space) leave a space for elided sign in numbers (% d)
 	0      pad with leading zeros rather than spaces
 

+ 61 - 0
core/fmt/fmt.odin

@@ -1048,6 +1048,65 @@ _fmt_int_128 :: proc(fi: ^Info, u: u128, base: int, is_signed: bool, bit_size: i
 	fi.zero = false
 	_pad(fi, s)
 }
+// Units of measurements:
+__MEMORY_LOWER := " b kib mib gib tib pib eib"
+__MEMORY_UPPER := " B KiB MiB GiB TiB PiB EiB"
+// Formats an integer value as bytes with the best representation.
+//
+// Inputs:
+// - fi: A pointer to an Info structure
+// - u: The integer value to format
+// - is_signed: A boolean indicating if the integer is signed
+// - bit_size: The bit size of the integer
+// - digits: A string containing the digits for formatting
+//
+_fmt_memory :: proc(fi: ^Info, u: u64, is_signed: bool, bit_size: int, units: string) {
+	abs, neg := strconv.is_integer_negative(u, is_signed, bit_size)
+
+	// Default to a precision of 2, but if less than a kb, 0
+	prec := fi.prec if (fi.prec_set || abs < mem.Kilobyte) else 2
+
+	div, off, unit_len := 1, 0, 1
+	for n := abs; n >= mem.Kilobyte; n /= mem.Kilobyte {
+		div *= mem.Kilobyte
+		off += 4
+
+		// First iteration is slightly different because you go from
+		// units of length 1 to units of length 2.
+		if unit_len == 1 {
+			off = 2
+			unit_len  = 3
+		}
+	}
+
+	// If hash, we add a space between the value and the suffix.
+	if fi.hash {
+		unit_len += 1
+	} else {
+		off += 1
+	}
+
+	amt := f64(abs) / f64(div)
+	if neg {
+		amt = -amt
+	}
+
+	buf: [256]byte
+	str := strconv.append_float(buf[:], amt, 'f', prec, 64)
+
+	// Add the unit at the end.
+	copy(buf[len(str):], units[off:off+unit_len])
+	str = string(buf[:len(str)+unit_len])
+	 
+	 if !fi.plus {
+	 	// Strip sign from "+<value>" but not "+Inf".
+	 	if str[0] == '+' && str[1] != 'I' {
+			str = str[1:] 
+		}
+	}
+
+	_pad(fi, str)
+}
 // Hex Values:
 __DIGITS_LOWER := "0123456789abcdefx"
 __DIGITS_UPPER := "0123456789ABCDEFX"
@@ -1096,6 +1155,8 @@ fmt_int :: proc(fi: ^Info, u: u64, is_signed: bool, bit_size: int, verb: rune) {
 			io.write_string(fi.writer, "U+", &fi.n)
 			_fmt_int(fi, u, 16, false, bit_size, __DIGITS_UPPER)
 		}
+	case 'm': _fmt_memory(fi, u, is_signed, bit_size, __MEMORY_LOWER)
+	case 'M': _fmt_memory(fi, u, is_signed, bit_size, __MEMORY_UPPER)
 
 	case:
 		fmt_bad_verb(fi, verb)

+ 1 - 1
core/mem/doc.odin

@@ -24,7 +24,7 @@ main :: proc() {
 	_main()
 
 	for _, leak in track.allocation_map {
-		fmt.printf("%v leaked %v bytes\n", leak.location, leak.size)
+		fmt.printf("%v leaked %m\n", leak.location, leak.size)
 	}
 	for bad_free in track.bad_free_array {
 		fmt.printf("%v allocation %p was freed badly\n", bad_free.location, bad_free.memory)

+ 2 - 0
core/mem/mem.odin

@@ -8,6 +8,8 @@ Kilobyte :: runtime.Kilobyte
 Megabyte :: runtime.Megabyte
 Gigabyte :: runtime.Gigabyte
 Terabyte :: runtime.Terabyte
+Petabyte :: runtime.Petabyte
+Exabyte  :: runtime.Exabyte
 
 set :: proc "contextless" (data: rawptr, value: byte, len: int) -> rawptr {
 	return runtime.memset(data, i32(value), len)

+ 2 - 0
core/runtime/core.odin

@@ -337,6 +337,8 @@ Kilobyte :: 1024 * Byte
 Megabyte :: 1024 * Kilobyte
 Gigabyte :: 1024 * Megabyte
 Terabyte :: 1024 * Gigabyte
+Petabyte :: 1024 * Terabyte
+Exabyte  :: 1024 * Petabyte
 
 // Logging stuff
 

+ 5 - 1
tests/core/Makefile

@@ -2,7 +2,8 @@ ODIN=../../odin
 PYTHON=$(shell which python3)
 
 all: download_test_assets image_test compress_test strings_test hash_test crypto_test noise_test encoding_test \
-	 math_test linalg_glsl_math_test filepath_test reflect_test os_exit_test i18n_test match_test c_libc_test net_test
+	 math_test linalg_glsl_math_test filepath_test reflect_test os_exit_test i18n_test match_test c_libc_test net_test \
+	 fmt_test
 
 download_test_assets:
 	$(PYTHON) download_assets.py
@@ -57,3 +58,6 @@ c_libc_test:
 
 net_test:
 	$(ODIN) run net -out:test_core_net
+
+fmt_test:
+	$(ODIN) run fmt -out:test_core_fmt

+ 59 - 0
tests/core/fmt/test_core_fmt.odin

@@ -0,0 +1,59 @@
+package test_core_fmt
+
+import "core:fmt"
+import "core:os"
+import "core:testing"
+import "core:mem"
+
+TEST_count := 0
+TEST_fail  := 0
+
+when ODIN_TEST {
+	expect  :: testing.expect
+	log     :: testing.log
+} else {
+	expect  :: proc(t: ^testing.T, condition: bool, message: string, loc := #caller_location) {
+		TEST_count += 1
+		if !condition {
+			TEST_fail += 1
+			fmt.printf("[%v] %v\n", loc, message)
+			return
+		}
+	}
+	log     :: proc(t: ^testing.T, v: any, loc := #caller_location) {
+		fmt.printf("[%v] ", loc)
+		fmt.printf("log: %v\n", v)
+	}
+}
+
+main :: proc() {
+	t := testing.T{}
+	test_fmt_memory(&t)
+
+	fmt.printf("%v/%v tests successful.\n", TEST_count - TEST_fail, TEST_count)
+	if TEST_fail > 0 {
+		os.exit(1)
+	}
+}
+
+test_fmt_memory :: proc(t: ^testing.T) {
+	check :: proc(t: ^testing.T, exp: string, format: string, args: ..any, loc := #caller_location) {
+		got := fmt.tprintf(format, ..args)
+		expect(t, got == exp, fmt.tprintf("(%q, %v): %q != %q", format, args, got, exp), loc)
+	}
+
+	check(t, "5b",        "%m",    5)
+	check(t, "5B",        "%M",    5)
+	check(t, "-5B",       "%M",    -5)
+	check(t, "3.00kib",   "%m",    mem.Kilobyte * 3)
+	check(t, "3kib",      "%.0m",  mem.Kilobyte * 3)
+	check(t, "3KiB",      "%.0M",  mem.Kilobyte * 3)
+	check(t, "3.000 mib", "%#.3m", mem.Megabyte * 3)
+	check(t, "3.50 gib",  "%#m",   u32(mem.Gigabyte * 3.5))
+	check(t, "01tib",     "%5.0m", mem.Terabyte)
+	check(t, "-1tib",     "%5.0m", -mem.Terabyte)
+	check(t, "2.50 pib",  "%#5.m", uint(mem.Petabyte * 2.5))
+	check(t, "1.00 EiB",  "%#M",   mem.Exabyte)
+	check(t, "255 B",     "%#M",   u8(255))
+	check(t, "0b",        "%m",    u8(0))
+}