Skip to content

Commit 92633a6

Browse files
committed
Expose GC API
Signed-off-by: Maksym Pavlenko <pavlenko.maksym@gmail.com>
1 parent ab019ed commit 92633a6

File tree

1 file changed

+368
-0
lines changed

1 file changed

+368
-0
lines changed

src/lua.zig

Lines changed: 368 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1218,6 +1218,247 @@ pub const Lua = struct {
12181218
pub inline fn setAssertHandler(handler: AssertHandler) void {
12191219
assert.setAssertHandler(handler);
12201220
}
1221+
1222+
/// Garbage collector control methods.
1223+
///
1224+
/// Luau uses an incremental garbage collector that performs work in small increments
1225+
/// without stopping the world. These methods provide fine-grained control over GC behavior.
1226+
pub const GC = struct {
1227+
lua: Lua,
1228+
1229+
/// Stop the garbage collector.
1230+
///
1231+
/// Disables automatic garbage collection. Memory will continue to be allocated
1232+
/// but no garbage collection cycles will run until `restart()` is called.
1233+
/// Use with caution as this can lead to excessive memory usage.
1234+
///
1235+
/// Example:
1236+
/// ```zig
1237+
/// const gc = lua.gc();
1238+
/// gc.stop(); // GC is now disabled
1239+
/// // ... do memory-intensive work
1240+
/// gc.restart(); // Re-enable GC
1241+
/// ```
1242+
pub fn stop(self: GC) void {
1243+
_ = self.lua.state.gc(.stop, 0);
1244+
}
1245+
1246+
/// Restart the garbage collector.
1247+
///
1248+
/// Re-enables automatic garbage collection after it was stopped with `stop()`.
1249+
/// The garbage collector will resume its normal incremental collection cycles.
1250+
///
1251+
/// Example:
1252+
/// ```zig
1253+
/// const gc = lua.gc();
1254+
/// gc.stop();
1255+
/// // ... do work with GC disabled
1256+
/// gc.restart(); // GC is active again
1257+
/// ```
1258+
pub fn restart(self: GC) void {
1259+
_ = self.lua.state.gc(.restart, 0);
1260+
}
1261+
1262+
/// Force a full garbage collection cycle.
1263+
///
1264+
/// Performs a complete garbage collection pass, freeing all unreachable objects.
1265+
/// This is more thorough than the incremental collection and may cause a brief pause.
1266+
/// Useful for reclaiming memory at application boundaries or after large operations.
1267+
///
1268+
/// Example:
1269+
/// ```zig
1270+
/// const gc = lua.gc();
1271+
/// // After loading large amounts of data
1272+
/// gc.collect(); // Free any unused memory
1273+
/// ```
1274+
pub fn collect(self: GC) void {
1275+
_ = self.lua.state.gc(.collect, 0);
1276+
}
1277+
1278+
/// Get the total memory usage in kilobytes.
1279+
///
1280+
/// Returns the total amount of memory currently used by the Lua state,
1281+
/// including all Lua objects, strings, tables, functions, etc.
1282+
/// The value is returned in kilobytes (1024 bytes).
1283+
///
1284+
/// Example:
1285+
/// ```zig
1286+
/// const gc = lua.gc();
1287+
/// const memory_kb = gc.count();
1288+
/// std.debug.print("Lua memory usage: {} KB\n", .{memory_kb});
1289+
/// ```
1290+
///
1291+
/// Returns: Memory usage in kilobytes
1292+
pub fn count(self: GC) i32 {
1293+
return self.lua.state.gc(.count, 0);
1294+
}
1295+
1296+
/// Get the fractional part of memory usage.
1297+
///
1298+
/// Returns the remainder of memory usage in bytes after the kilobyte count.
1299+
/// Combined with `count()`, this gives precise memory usage:
1300+
/// `total_bytes = count() * 1024 + countBytes()`
1301+
///
1302+
/// Example:
1303+
/// ```zig
1304+
/// const gc = lua.gc();
1305+
/// const kb = gc.count();
1306+
/// const bytes = gc.countBytes();
1307+
/// const total_bytes = kb * 1024 + bytes;
1308+
/// std.debug.print("Precise memory usage: {} bytes\n", .{total_bytes});
1309+
/// ```
1310+
///
1311+
/// Returns: Additional bytes beyond the kilobyte count (0-1023)
1312+
pub fn countBytes(self: GC) i32 {
1313+
return self.lua.state.gc(.countb, 0);
1314+
}
1315+
1316+
/// Check if the garbage collector is currently running.
1317+
///
1318+
/// Returns `true` if automatic garbage collection is enabled and active,
1319+
/// `false` if it has been stopped with `stop()` or is otherwise disabled.
1320+
///
1321+
/// Example:
1322+
/// ```zig
1323+
/// const gc = lua.gc();
1324+
/// if (gc.isRunning()) {
1325+
/// std.debug.print("GC is active\n");
1326+
/// } else {
1327+
/// std.debug.print("GC is stopped\n");
1328+
/// }
1329+
/// ```
1330+
///
1331+
/// Returns: `true` if GC is running, `false` otherwise
1332+
pub fn isRunning(self: GC) bool {
1333+
return self.lua.state.gc(.isrunning, 0) != 0;
1334+
}
1335+
1336+
/// Perform a single incremental garbage collection step.
1337+
///
1338+
/// Runs one step of the incremental garbage collector. The `size` parameter
1339+
/// controls how much work to do in this step (larger values = more work).
1340+
/// Returns `true` if the GC cycle completed, `false` if more steps are needed.
1341+
///
1342+
/// This is useful for manual control over GC timing in performance-critical code.
1343+
///
1344+
/// Example:
1345+
/// ```zig
1346+
/// const gc = lua.gc();
1347+
/// gc.stop(); // Disable automatic GC
1348+
///
1349+
/// while (!gc.step(100)) {
1350+
/// // GC cycle not complete, do some work
1351+
/// // ... application work ...
1352+
/// }
1353+
/// // GC cycle completed
1354+
/// ```
1355+
///
1356+
/// Parameters:
1357+
/// - `size`: Amount of work to perform (typically 100-1000)
1358+
///
1359+
/// Returns: `true` if GC cycle completed, `false` if more work remains
1360+
pub fn step(self: GC, size: i32) bool {
1361+
return self.lua.state.gc(.step, size) != 0;
1362+
}
1363+
1364+
/// Set the garbage collection goal.
1365+
///
1366+
/// Controls when the next GC cycle should start based on memory usage.
1367+
/// The goal is specified as a percentage of current memory usage.
1368+
/// For example, a goal of 200 means GC will start when memory usage
1369+
/// doubles from the current level.
1370+
///
1371+
/// Lower values trigger GC more frequently (less memory usage, more CPU overhead).
1372+
/// Higher values trigger GC less frequently (more memory usage, less CPU overhead).
1373+
///
1374+
/// Example:
1375+
/// ```zig
1376+
/// const gc = lua.gc();
1377+
/// gc.setGoal(150); // Start GC when memory increases by 50%
1378+
/// ```
1379+
///
1380+
/// Parameters:
1381+
/// - `goal`: GC trigger threshold as percentage (typically 100-300)
1382+
///
1383+
/// Returns: Previous goal value
1384+
pub fn setGoal(self: GC, goal: i32) i32 {
1385+
return self.lua.state.gc(.setgoal, goal);
1386+
}
1387+
1388+
/// Set the garbage collection step multiplier.
1389+
///
1390+
/// Controls how much work the GC does in each incremental step relative
1391+
/// to memory allocation. Higher values make GC more aggressive (more CPU
1392+
/// overhead but lower memory usage), lower values make it less aggressive.
1393+
///
1394+
/// The default value is typically around 200. Values between 100-500 are common.
1395+
///
1396+
/// Example:
1397+
/// ```zig
1398+
/// const gc = lua.gc();
1399+
/// gc.setStepMul(300); // Make GC more aggressive
1400+
/// ```
1401+
///
1402+
/// Parameters:
1403+
/// - `stepmul`: Step multiplier (typically 100-500)
1404+
///
1405+
/// Returns: Previous step multiplier value
1406+
pub fn setStepMul(self: GC, stepmul: i32) i32 {
1407+
return self.lua.state.gc(.setstepmul, stepmul);
1408+
}
1409+
1410+
/// Set the garbage collection step size.
1411+
///
1412+
/// Controls the size of each incremental GC step in bytes. Larger step sizes
1413+
/// mean fewer but larger GC pauses, smaller step sizes mean more frequent
1414+
/// but shorter pauses.
1415+
///
1416+
/// This fine-tunes the incremental GC behavior for specific performance needs.
1417+
///
1418+
/// Example:
1419+
/// ```zig
1420+
/// const gc = lua.gc();
1421+
/// gc.setStepSize(1024); // 1KB per GC step
1422+
/// ```
1423+
///
1424+
/// Parameters:
1425+
/// - `stepsize`: Size of each GC step in bytes
1426+
///
1427+
/// Returns: Previous step size value
1428+
pub fn setStepSize(self: GC, stepsize: i32) i32 {
1429+
return self.lua.state.gc(.setstepsize, stepsize);
1430+
}
1431+
};
1432+
1433+
/// Get a garbage collector control interface.
1434+
///
1435+
/// Returns a GC control object that provides methods for managing Luau's
1436+
/// incremental garbage collector. Use this to monitor memory usage,
1437+
/// tune GC performance, or manually control collection timing.
1438+
///
1439+
/// Example:
1440+
/// ```zig
1441+
/// const lua = try Lua.init(null);
1442+
/// defer lua.deinit();
1443+
///
1444+
/// const gc = lua.gc();
1445+
///
1446+
/// // Check current memory usage
1447+
/// const memory_kb = gc.count();
1448+
/// std.debug.print("Memory usage: {} KB\n", .{memory_kb});
1449+
///
1450+
/// // Force garbage collection
1451+
/// gc.collect();
1452+
///
1453+
/// // Check memory after collection
1454+
/// const memory_after = gc.count();
1455+
/// std.debug.print("Memory after GC: {} KB\n", .{memory_after});
1456+
/// ```
1457+
///
1458+
/// Returns: GC control interface
1459+
pub fn gc(self: Self) GC {
1460+
return GC{ .lua = self };
1461+
}
12211462
};
12221463

12231464
const expect = std.testing.expect;
@@ -1390,3 +1631,130 @@ test "struct and array integration" {
13901631
const points_length = try lua.eval("return #config.points", .{}, i32);
13911632
try expectEq(points_length, 2);
13921633
}
1634+
1635+
test "garbage collector operations" {
1636+
const lua = try Lua.init(&std.testing.allocator);
1637+
defer lua.deinit();
1638+
1639+
lua.openLibs();
1640+
1641+
const gc = lua.gc();
1642+
1643+
// Test that GC is initially running
1644+
try expect(gc.isRunning());
1645+
1646+
// Test memory counting
1647+
const initial_memory = gc.count();
1648+
const initial_bytes = gc.countBytes();
1649+
try expect(initial_memory >= 0);
1650+
try expect(initial_bytes >= 0 and initial_bytes < 1024);
1651+
1652+
// Create some objects to increase memory usage
1653+
const globals = lua.globals();
1654+
try globals.set("test_table", lua.createTable(.{ .rec = 100 }));
1655+
1656+
_ = try lua.eval(
1657+
\\for i = 1, 100 do
1658+
\\ test_table[i] = "string number " .. i
1659+
\\end
1660+
, .{}, void);
1661+
1662+
// Memory should have increased
1663+
const after_alloc_memory = gc.count();
1664+
try expect(after_alloc_memory >= initial_memory);
1665+
1666+
// Test stopping and restarting GC
1667+
gc.stop();
1668+
try expect(!gc.isRunning());
1669+
1670+
gc.restart();
1671+
try expect(gc.isRunning());
1672+
1673+
// Test forcing garbage collection
1674+
gc.collect();
1675+
const after_collect_memory = gc.count();
1676+
1677+
// Memory might be same or less after collection
1678+
// (depends on what was actually collectible)
1679+
try expect(after_collect_memory >= 0);
1680+
1681+
// Test GC stepping
1682+
gc.stop();
1683+
try expect(!gc.isRunning());
1684+
1685+
// Create more garbage
1686+
_ = try lua.eval(
1687+
\\local temp = {}
1688+
\\for i = 1, 50 do
1689+
\\ temp[i] = {}
1690+
\\ for j = 1, 10 do
1691+
\\ temp[i][j] = "temp string " .. i .. "," .. j
1692+
\\ end
1693+
\\end
1694+
\\temp = nil
1695+
, .{}, void);
1696+
1697+
// Perform stepped collection
1698+
var steps: u32 = 0;
1699+
while (!gc.step(100) and steps < 10) {
1700+
steps += 1;
1701+
}
1702+
1703+
// Should have completed within reasonable steps
1704+
try expect(steps < 10);
1705+
1706+
// Test GC parameter setting
1707+
const old_goal = gc.setGoal(150);
1708+
try expect(old_goal > 0);
1709+
1710+
const old_stepmul = gc.setStepMul(250);
1711+
try expect(old_stepmul > 0);
1712+
1713+
const old_stepsize = gc.setStepSize(2048);
1714+
try expect(old_stepsize > 0);
1715+
1716+
// Restore original parameters
1717+
_ = gc.setGoal(old_goal);
1718+
_ = gc.setStepMul(old_stepmul);
1719+
_ = gc.setStepSize(old_stepsize);
1720+
1721+
gc.restart();
1722+
try expect(gc.isRunning());
1723+
}
1724+
1725+
test "gc memory measurement precision" {
1726+
const lua = try Lua.init(&std.testing.allocator);
1727+
defer lua.deinit();
1728+
1729+
const gc = lua.gc();
1730+
1731+
// Test precise memory measurement
1732+
const kb = gc.count();
1733+
const bytes = gc.countBytes();
1734+
const total_bytes = kb * 1024 + bytes;
1735+
1736+
try expect(kb >= 0);
1737+
try expect(bytes >= 0 and bytes < 1024);
1738+
try expect(total_bytes >= 0);
1739+
1740+
// Allocate a known amount and verify memory increases
1741+
const globals = lua.globals();
1742+
const large_table = lua.createTable(.{ .arr = 1000 });
1743+
defer large_table.deinit();
1744+
1745+
try globals.set("large_table", large_table);
1746+
1747+
// Fill table with data
1748+
_ = try lua.eval(
1749+
\\for i = 1, 1000 do
1750+
\\ large_table[i] = i
1751+
\\end
1752+
, .{}, void);
1753+
1754+
const kb_after = gc.count();
1755+
const bytes_after = gc.countBytes();
1756+
const total_bytes_after = kb_after * 1024 + bytes_after;
1757+
1758+
// Memory should have increased significantly
1759+
try expect(total_bytes_after > total_bytes);
1760+
}

0 commit comments

Comments
 (0)