Skip to content

Commit f494c9a

Browse files
committed
fix: suspend inactive capture and bound remote discovery
1 parent b4e99e5 commit f494c9a

16 files changed

Lines changed: 655 additions & 105 deletions

‎CONTRIBUTING.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ There exist different `project.json` files for different purposes:
1818
Please use the [issue tracker](https://github.com/Pyseph/PacketProfiler/issues) to report any bugs or file feature requests.
1919

2020
## Developing
21+
22+
Run `lune run tests/performance.luau` with Lune 0.10.4 to check capture lifecycle, bounded discovery, remote subscription cleanup, and Studio bridge demand. These tests load the source modules with mocked engine signals and instance trees; verify UI behavior and frame times separately in Studio. Run `selene src build` and build both `buildplugin.project.json` and `buildrelease.project.json` with Rojo before release.
2123
Pull requests are welcome! For major changes, please open an issue first to discuss what you would like to change.
2224

2325
## Adding a new data type

‎README.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ The rolling timeline uses timestamped 60 Hz buckets, so its 256 bars consistentl
2222

2323
The interface is rendered with Vide. The timeline is a mirrored cyclic strip: each commit writes one persistent bar slot and shifts the strip, so existing heights remain untouched until their frames genuinely leave the 256-frame window. This avoids full-chart reconciliation and render-frame polling.
2424

25+
Capture runs while either profiler window is open and capture is unpaused. Closing both windows or pausing disconnects capture adapters, remote discovery, and the capture clock; reopening preserves committed frames and starts a fresh capture bucket. Hidden views unmount their contents. The Studio Edit instance does not discover remotes. Play-client remote discovery walks service children incrementally, with at most 256 instances per heartbeat and a 1 ms target budget, without `QueryDescendants` or `GetDescendants`. Existing remotes begin recording as discovery reaches them; newly added remotes connect immediately during capture.
26+
2527
![image](https://github.com/Pyseph/PacketProfiler/assets/45090858/9a68dc84-0ad5-4358-9e79-8750d9e40b2e)
2628
![image](https://user-images.githubusercontent.com/45090858/181864397-9b9d5e82-72fe-4bee-b29f-9b8e5a16aa4d.png)
2729

@@ -40,6 +42,8 @@ Requiring the in-game package returns the running profiler. In a Studio client w
4042

4143
Layers describe a source of traffic and receive an `Emit` function. The value returned by `Start` is used as the layer's cleanup function.
4244

45+
`Start` can run again after capture resumes. Return cleanup that disconnects the adapter's listeners and cancels its background work. Studio-bridged adapters follow capture demand from the Play and Edit windows, and only flush packets when a batch is pending. Update the plugin and in-game package together for bridge protocol version 4.
46+
4347
```luau
4448
local ReplicatedStorage = game:GetService("ReplicatedStorage")
4549
local PacketProfiler = require(ReplicatedStorage.Packages.PacketProfiler)

‎build/main.server.luau‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,9 @@ local PacketProfiler = Toolbar:CreateButton("Open Profiler", "Open Profiler Grap
2525
local PacketChart = Toolbar:CreateButton("Open Chart", "Open Pie Chart", "rbxassetid://10283406077")
2626

2727
local profiler = Profiler.new()
28-
profiler:RegisterLayer(RemoteLayer)
29-
profiler:Start()
28+
if isTestDataModel then
29+
profiler:RegisterLayer(RemoteLayer)
30+
end
3031
local stopStudioBridge
3132
if isTestDataModel then
3233
local StudioBridgeTransport = require(Modules.StudioBridgeTransport)

‎src/Components/MainPlugin.luau‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,19 @@ local function MainPlugin(props)
1111
return Vide.mount(function()
1212
local theme = StudioTheme()
1313
local state = UIState(props)
14-
local profilerView = PacketProfilerView({ State = state, Theme = theme })
14+
Vide.effect(function()
15+
if state.ProfilerEnabled() then
16+
Vide.cleanup(Vide.mount(function()
17+
PacketProfilerView({ State = state, Theme = theme })
18+
end))
19+
end
20+
end)
1521
local chartView = PacketChart({
1622
Plugin = props.Plugin,
1723
State = state,
1824
Theme = theme,
1925
})
2026

21-
Vide.cleanup(profilerView)
2227
Vide.cleanup(chartView)
2328
end)
2429
end

‎src/Components/PacketChart.luau‎

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ local create = Vide.create
2323
local source = Vide.source
2424
local switch = Vide.switch
2525

26-
local function PacketChart(props)
26+
local function PacketChartContents(props, target)
2727
local state = props.State
2828
local profiler = state.Profiler
2929
local arcs = source({})
@@ -210,8 +210,14 @@ local function PacketChart(props)
210210
})
211211
end
212212

213+
return holder(target)
214+
end
215+
216+
local function PacketChart(props)
217+
local state = props.State
218+
local parent
213219
if Packages.IsPlugin then
214-
local widget = StudioWidget({
220+
parent = StudioWidget({
215221
DefaultSize = Vector2.new(520, 420),
216222
Enabled = state.ChartEnabled,
217223
InitialDockState = Enum.InitialDockState.Float,
@@ -222,18 +228,23 @@ local function PacketChart(props)
222228
WidgetTitle = "Packet details",
223229
ZIndexBehavior = Enum.ZIndexBehavior.Sibling,
224230
})
225-
return holder(widget)
231+
else
232+
parent = create("ScreenGui")({
233+
Enabled = state.ChartEnabled,
234+
IgnoreGuiInset = true,
235+
Parent = Players.LocalPlayer.PlayerGui,
236+
ResetOnSpawn = false,
237+
ZIndexBehavior = Enum.ZIndexBehavior.Sibling,
238+
})
226239
end
227-
228-
local screen = create("ScreenGui")({
229-
Enabled = state.ChartEnabled,
230-
IgnoreGuiInset = true,
231-
Parent = Players.LocalPlayer.PlayerGui,
232-
ResetOnSpawn = false,
233-
ZIndexBehavior = Enum.ZIndexBehavior.Sibling,
234-
})
235-
holder(screen)
236-
return screen
240+
Vide.effect(function()
241+
if state.ChartEnabled() then
242+
cleanup(Vide.mount(function()
243+
PacketChartContents(props, parent)
244+
end))
245+
end
246+
end)
247+
return parent
237248
end
238249

239250
return PacketChart

‎src/Components/PacketCircleArcs.luau‎

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -118,13 +118,17 @@ local function PacketCircleArcs(props)
118118
effect(function()
119119
read(props.Arcs)
120120
actualAlpha(0)
121+
local elapsed = 0
122+
local connection
123+
connection = RunService.RenderStepped:Connect(function(deltaTime)
124+
elapsed = math.min(elapsed + deltaTime * 2, 1)
125+
actualAlpha(elapsed)
126+
if elapsed == 1 then
127+
connection:Disconnect()
128+
end
129+
end)
130+
cleanup(connection)
121131
end)
122-
cleanup(RunService.RenderStepped:Connect(function(deltaTime)
123-
local alpha = actualAlpha()
124-
if alpha < 1 then
125-
actualAlpha(math.min(alpha + deltaTime * 2, 1))
126-
end
127-
end))
128132

129133
return create("Frame")({
130134
AnchorPoint = Vector2.new(0.5, 0.5),

‎src/Modules/BridgeProtocol.luau‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@ local BridgeProtocol = {
55
OutboundName = "PacketProfilerBridgeOutbound.profiler",
66
OwnerAttribute = "PacketProfilerStudioBridge",
77
VersionAttribute = "PacketProfilerBridgeVersion",
8-
Version = 3,
8+
CaptureAttribute = "PacketProfilerCapturing",
9+
CaptureChanged = "CaptureChanged",
10+
RequestCaptureState = "RequestCaptureState",
11+
Version = 4,
912
RegisterLayer = "RegisterLayer",
1013
RegistrationResult = "RegistrationResult",
1114
RecordBatch = "RecordBatch",

‎src/Modules/BridgeReceiver.luau‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ end
133133
function BridgeReceiver:Receive(ownerId, command, first, second, third)
134134
if command == BridgeProtocol.RegisterLayer then
135135
self:_registerLayer(ownerId, first, second, third)
136-
elseif command == BridgeProtocol.RecordBatch and type(second) == "table" then
136+
elseif command == BridgeProtocol.RecordBatch and self.Profiler:IsCapturing() and type(second) == "table" then
137137
for _, item in second do
138138
self:_record(ownerId, first, item)
139139
end

‎src/Modules/Profiler.luau‎

Lines changed: 58 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ function Profiler.new()
4444
FrameBatchCommitted = Signal.new(),
4545
LayersChanged = Signal.new(),
4646
PauseChanged = Signal.new(),
47+
CaptureChanged = Signal.new(),
4748
ViewChanged = Signal.new(),
4849
SelectionChanged = Signal.new(),
4950
TopFramesChanged = Signal.new(),
@@ -159,7 +160,7 @@ function Profiler:RegisterAddon(addon)
159160
end
160161

161162
function Profiler:Record(layerId, packet)
162-
if self.Paused then
163+
if not self:IsCapturing() then
163164
return false
164165
end
165166
local layer = self._registry:Get(layerId)
@@ -216,69 +217,94 @@ function Profiler:SetPaused(paused)
216217
return
217218
end
218219
self.Paused = paused
219-
if not paused then
220-
self._frames:Seek(os.clock())
221-
end
220+
self:_updateCapture()
222221
self.PauseChanged:Fire(paused)
223222
end
224223

224+
function Profiler:IsCapturing()
225+
return self._running and not self.Paused
226+
end
227+
225228
function Profiler:SelectFrame(frame)
226229
self.SelectedFrame = frame
227230
self.SelectionChanged:Fire(frame and self:GetViewFrame(frame) or nil)
228231
end
229232

230233
function Profiler:_commitFrame()
231-
if self.Paused then
234+
if not self:IsCapturing() then
232235
return nil
233236
end
234237

235238
local frame = self._frames:Commit()
236-
if
237-
self._topFrames:Record(frame, self._registry:GetAll(), function(targetFrame, view)
239+
local topFramesChanged = frame.PacketCount > 0
240+
and self._topFrames:Record(frame, self._registry:GetAll(), function(targetFrame, view)
238241
return self._frames:GetSize(targetFrame, view)
239242
end)
240-
then
241-
self.TopFramesChanged:Fire()
242-
end
243243
self._hooks:Run("FrameCommitted", frame)
244244
self.FrameCommitted:Fire(frame, self.Frames)
245-
return frame
245+
return frame, topFramesChanged
246246
end
247247

248248
function Profiler:Start()
249249
if self._running then
250250
return
251251
end
252252
self._running = true
253+
self:_updateCapture()
254+
end
255+
256+
function Profiler:Stop()
257+
if not self._running then
258+
return
259+
end
260+
self._running = false
261+
self:_updateCapture()
262+
end
263+
264+
function Profiler:_updateCapture()
265+
if self._stepConnection then
266+
self._stepConnection:Disconnect()
267+
self._stepConnection = nil
268+
end
269+
if not self:IsCapturing() then
270+
self._registry:Stop()
271+
self.CaptureChanged:Fire(false)
272+
return
273+
end
274+
self._frames:Seek(os.clock())
253275
self._registry:Start()
254276

255277
local stepSignal = RunService:IsClient() and RunService.RenderStepped or RunService.Heartbeat
256278
local elapsed = 0
257-
table.insert(
258-
self._cleanup,
259-
stepSignal:Connect(function(deltaTime)
260-
elapsed += deltaTime
261-
local framesDue = math.floor(elapsed / FRAME_INTERVAL)
262-
if framesDue > 0 then
263-
elapsed -= framesDue * FRAME_INTERVAL
264-
local skippedFrames = math.max(framesDue - MAX_FRAMES, 0)
265-
self._frames:Skip(skippedFrames)
266-
local committedFrames = table.create(framesDue - skippedFrames)
267-
for _ = 1, framesDue - skippedFrames do
268-
local frame = self:_commitFrame()
269-
if frame then
270-
table.insert(committedFrames, frame)
271-
end
272-
end
273-
if #committedFrames > 0 then
274-
self.FrameBatchCommitted:Fire(committedFrames, self.Frames)
279+
self._stepConnection = stepSignal:Connect(function(deltaTime)
280+
elapsed += deltaTime
281+
local framesDue = math.floor(elapsed / FRAME_INTERVAL)
282+
if framesDue > 0 then
283+
elapsed -= framesDue * FRAME_INTERVAL
284+
local skippedFrames = math.max(framesDue - MAX_FRAMES, 0)
285+
self._frames:Skip(skippedFrames)
286+
local committedFrames = table.create(framesDue - skippedFrames)
287+
local topFramesChanged = false
288+
for _ = 1, framesDue - skippedFrames do
289+
local frame, changed = self:_commitFrame()
290+
topFramesChanged = topFramesChanged or changed
291+
if frame then
292+
table.insert(committedFrames, frame)
275293
end
276294
end
277-
end)
278-
)
295+
if topFramesChanged then
296+
self.TopFramesChanged:Fire()
297+
end
298+
if #committedFrames > 0 then
299+
self.FrameBatchCommitted:Fire(committedFrames, self.Frames)
300+
end
301+
end
302+
end)
303+
self.CaptureChanged:Fire(true)
279304
end
280305

281306
function Profiler:Destroy()
307+
self:Stop()
282308
for _, cleanup in self._addons do
283309
if cleanup then
284310
Cleanup.run(cleanup)
@@ -292,6 +318,7 @@ function Profiler:Destroy()
292318
self.FrameBatchCommitted:DisconnectAll()
293319
self.LayersChanged:DisconnectAll()
294320
self.PauseChanged:DisconnectAll()
321+
self.CaptureChanged:DisconnectAll()
295322
self.ViewChanged:DisconnectAll()
296323
self.SelectionChanged:DisconnectAll()
297324
self.TopFramesChanged:DisconnectAll()

0 commit comments

Comments
 (0)