For this 14-field test record, linkset data holds about 660 JSON objects or 6,000 packed buffers. Compare storage savings with script memory and code costs.
This experiment compares the space a fixed-field record takes as a JSON object, JSON array, CSV, and packed buffer. The storage tests save 500 records. Separate tests measure script memory with one or 100 records loaded and time repeated reads of four fields.
The sample is a lighting preset with 14 fields: a light group, mode, intensity, glow, radius, three RGB color channels, fade time, and five on/off switches for enabled, fullbright, cast light, sync, and owner-only. The code and figures call each test record a preset.
The records go into linkset data, the object's built-in key-value storage. It holds text, and every script in the object shares its 128 KiB. The storage tests save each record under its own key. The walkthrough later loads one at a time.
Saved as JSON, this record takes about 198 bytes with its key. An empty store holds 662 of them, and 500 records leave 32,130 bytes for everything else in the object. Packed into buffers, each record takes about 21 bytes, and 6,004 fit.
Field names, quotes, and colons take 127 of the object's 193 characters. Remove them and the object becomes the array. CSV drops the brackets and quotes too, and writes the switches as 1 and 0. The 11-byte buffer becomes 16 characters of base64. Storage keys such as p:42 add a few more characters to each.
Packing needs extra code. You decide where each value goes, how precise it needs to be, and how to read it back.
These capacities depend on the sample's field names, value ranges, and key lengths. The same comparison works for other fixed-field records, but their storage savings will differ.
Values are small numbers or on/off switches with known ranges
Your own scripts write and read the data, so both sides share one layout
The layout is settled, so each value can keep a fixed byte position
In between: JSON arrays and CSV
Arrays such as [2, "pulse", ...] drop the field names but stay JSON. The sample record takes 66 characters instead of 193, so 1,834 fit in an empty store. Each array decodes into a 256-byte table, compared with 544 bytes for named fields. The writer and reader must agree on the field order.
CSV drops the brackets and quotes and writes switches as 1 and 0, as in 2,pulse,0.75,.... The sample record takes 45 characters, and 2,589 fit. Reading it back with string.split or ll.CSV2List returns every field as a string, so you convert each one yourself. ll.List2CSV puts a space after each comma, so join the fields with table.concat(values, ",") instead.
JSON repeats field names in every record. In "red":255, the number takes three bytes, while the field name, quotes, and colon take another six. A packed buffer stores that color channel in one byte.
Linkset data only holds text, so you save the buffer as base64. Base64 turns every three bytes into four characters, so the 11-byte record becomes 16 characters.
Saving uses linkset data. Loading uses script memory, a separate budget of 128 KiB for each script's code and data. The totals below include short numbered keys such as p:42, but no record names.
500 presets in linkset data
128 KiB, shared by every script in the object
Used
0 B
Free
131,072 B
Fit when empty
0
032 KiB64 KiB96 KiB128 KiB
Packing leaves 120,630 B free for names and other settings. JSON objects leave 32,130 B.One preset in script memory: 544 B as an object, 256 B as an array, or 119 B packed, including the 96 B mode lookup. I calculated the memory sizes. All four storage totals, keys included, match tests in Second Life.
Most of the saving here is in linkset data. With one sample record loaded, packing saves 425 bytes of data. In the simulator tests, the extra packing code cost more than that. These sizes also leave out temporary memory used during loading and saving. Measure your script has the whole-script totals.
A buffer gives every field a fixed position. The sample record uses 11 bytes: eight one-byte values for the group, mode, intensity, glow, radius, and RGB channels; two bytes for fade time; and one byte for all five switches.
Set each field's range and precision before assigning it space. The guide below shows the available types and how the sample uses them. The cards use the ranges chosen for this test schema.
Choose a type for each value
One square = one byte
Used by this preset
8-bit whole numbers
1 B
u8 · zero and up
0 to 255
i8 · allows negatives
-128 to 127
Group and RGB 0–255; mode 0–3; intensity and glow 0–100%; radius 1–20 whole meters.
16-bit whole numbers
2 B
u16 · zero and up
0 to 65,535
i16 · allows negatives
-32,768 to 32,767
Fade time 0–65,535 ms. A 1,500 ms fade takes two bytes.
On/off switches
1 B
One byte has 8 bits, so it can hold up to 8 separate switches
00001101
The preset's five switches fit in one byte, with three bits left over
Other types
32-bit whole numbers
4 B
u32 · zero and up
0 to 4,294,967,295
i32 · allows negatives
-2,147,483,648 to 2,147,483,647
For larger values, such as an item ID of 100,000.
Decimals
f324 B
About 7 significant digits
f648 B
About 15 significant digits
More precision costs more bytes. This preset stores whole percentages in u8 fields instead.
Text
Variable length. ASCII uses one byte per character; UTF-8 can use more. Packing keeps those character bytes.
Red3 B
灯3 B
Keep this preset's name in its storage key and load labels one page at a time.
Squares count value bytes, excluding buffer overhead and any text-length fields.
Intensity and glow use whole percentages. An input of 0.753 becomes 75 in the buffer and reads back as 0.75. Rounding loses precision, so use a wider field if those extra digits matter. Rounding a number in a Lua table saves nothing, because every number takes the same slot.
Validate before writing. A value outside an integer field's range can wrap instead of failing. Round before converting to an integer, too. For example, 0.29 * 100 comes out just below 29, so writing it directly stores 28. These setters round, write the byte, and update the value the rest of the script uses. They omit range checks, but the complete example includes them:
This script starts with one sample record already saved under its own key. It names the byte positions it uses, then loads, edits, and saves the record.
Load, edit, and save one preset
The 11 bytes of the sample preset, and the bytes the script reads and writes
MODE, INTENSITY, and FLAGS name the colored offsets. The script starts with a preset already saved under its key, reads two values, turns fullbright on, and writes the buffer back as base64.
A few lines deserve a note:
The true argument to llbase64.decode asks for a buffer instead of a string
Lua tables start at 1, so the Lua adds 1 to the stored mode before looking it up in MODES
The switches byte holds all five settings, and OR sets the fullbright bit while keeping the other four
A write can return a no-change status as well as success, so savedOk accepts both, and a failed write needs a retry
If you edit records independently, keep each under its own key so an edit only rewrites that record. Combining every record under one key means loading, decoding, and rewriting the whole value. A save also holds the buffer and its base64 text at once, so a combined key keeps two copies of every record in memory.
Load base64 from storage rather than pasting it into the script. A pasted constant stays in script memory after decoding.
The complete one-record example puts these steps together. It creates sample records with default values and loads one page of names at a time. You can select a record, edit it, and save it back. Its behavior checks passed in Second Life.
When a loop repeatedly uses the same fields, convert them once after loading the record. For this sample, that includes dividing the percentages by 100 and looking up the mode name. Its RGB bytes also need conversion to linear color before use with point lights. Keep the results in variables and update them when a value changes, as the setters above do.
Reading four settings 200,000 times
Slowest first · ratios compare with cached values · measured in Second Life
MedianRangeRatio
Buffer reads
buffer.readu8(record, INTENSITY) / 100
2.529 s median
Range: 2.466– to 2.680 s
3.0×
Table fields
preset.intensity
1.661 s median
Range: 1.555– to 1.840 s
1.9×
Cached values
intensity
0.856 s median
Range: 0.821– to 0.935 s
baseline
0123
Time in s · lower is better
Each pass reads intensity, glow, radius, and one switch, then uses them. Across Second Life's Agni and Aditi environments, a pass took about 13 µs with buffer reads and 4 µs with cached values. Each trial alternates the three patterns for 10 rounds each. Dots show the median of the 4 trial medians. The times include simulator scheduling and cover the repeated reads, not the complete example script.
In this test, reading directly from a buffer took about three times as long as reading variables. Reading table fields took almost twice as long as reading variables. A buffer read calls a function and converts the byte every time. A table field holds a value the script already converted, and a variable skips the field lookup too.
You can pack stored records and keep frequently read values in variables. The test measures the read pattern separately from storage and the rest of the script.
A buffer still stores every byte of the text. A 20-byte name costs 36 bytes as a string, before any list slot that holds it. A dialog shows at most twelve buttons, so load the current page of labels instead of keeping every name in memory.
A record's key can hold its name. In the example, p:Evening holds one record's base64 data, so the value doesn't repeat the name. Names make the keys longer. With 20-character names, the 500 packed test records take 19,000 bytes instead of 10,442, which still leaves about 109 KiB free.
Page through the keys with these calls:
ll.LinksetDataFindKeys("^p:", 1, 10) returns up to ten matching keys alphabetically
Change the start from 1 to 11 for the next page
ll.LinksetDataCountFound("^p:") counts matches without loading the keys
Example preset browser
Choose a preset, page 1 of 3
Amber
Aurora
Bedtime
Candle
Cinema
Cool white
Dawn
Disco
Dusk
Evening
Ignore
ll.LinksetDataCountFound("^p:")
23 keys, 3 pages
ll.LinksetDataFindKeys("^p:", 1, 10)
"p:Amber" … "p:Evening"
Each page starts 10 keys later: 1, 11, 21. The buttons show each key without p:.
Searches match keys, not values. Strip p: for button labels and keep the page's keys to look up the selected record. Ten names per page leave two buttons for navigation, and each name must fit the 24-byte label limit.
The sizes above count data, not the code that reads it. In the simulator tests, the whole buffer script with one record loaded used more memory than either JSON script. Its data was smaller, but the packing code outweighed the saving. With 100 records loaded together, the buffer script used about a tenth of the memory of the script that used JSON objects. The 100-record case matches the cache example under Details. These results apply to the sample schema and test scripts.
Whole script at the decoded checkpoint
1 preset
JSON objects5,461 B
JSON arrays4,714 B
Buffer5,839 B
0 B8 KiB
100 presets
JSON objects80,116 B
JSON arrays38,284 B
Buffer8,389 B
0 B128 KiB
Each panel has its own scale · Lower uses less memory
ll.GetUsedMemory() after decoding, in Second Life. Each total includes the test script's code, its input text, and its correctness checks, so it is not the complete example script's memory use.
Check your own script before committing to the extra packing code:
Time loading a record separately from repeated field reads
Test saves as often as your script will make them
Count everything that stays loaded, including buffers, strings, lookup tables, and the code that reads them
ll.GetUsedMemory() reports memory at the moment you call it. It misses anything allocated and freed between calls, so a checkpoint can't show an operation's peak.
An appended 285-entry list in SLua uses 384 slots at 16 bytes each. I test when table.create saves memory and how to size my HUD's paint lists.
Objects··9 min
What does named access cost?
A shared metatable lets code read arrays and buffers by field name. In the test, 100 records use about of the script's memory with named fields, or as buffers with names.