Skip to content

Commit

Permalink
feat(core/profiler): add natives exposing start/stop recording
Browse files Browse the repository at this point in the history
- currently trying to do recordings can only be done via console commands, which can be tedious
- adds `PROFILER_START_RECORDING` and `PROFILER_STOP_RECORDING` to allow automating profiler captures
- adds `PROFILER_SAVE_TO_JSON` and `PROFILER_SAVE_TO_MSGPACK` to allow for automated saving of profiler recordings
  • Loading branch information
AvarianKnight committed Oct 25, 2024
1 parent 49406c4 commit 6dad489
Show file tree
Hide file tree
Showing 5 changed files with 213 additions and 75 deletions.
228 changes: 153 additions & 75 deletions code/components/citizen-scripting-core/src/Profiler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,98 @@ void PrintEvents(const TContainer& evs) {
}
}

namespace profiler
{
static auto MakeVfsStream = [](const std::string& path) -> fwRefContainer<vfs::Stream>
{
std::string outFn = path;

#ifndef IS_FXSERVER
outFn = "citizen:/profiler/" + outFn;
#endif

auto vfsDevice = vfs::GetDevice(outFn);

if (!vfsDevice.GetRef())
{
console::PrintError("cmd", "Invalid path %s.\n", path);
return nullptr;
}

auto handle = vfsDevice->Create(outFn);

if (handle == vfs::Device::InvalidHandle)
{
console::PrintError("cmd", "Invalid path %s.\n", path);
return nullptr;
}

return new vfs::Stream(vfsDevice, handle);
};

auto SaveToJSON = ExecuteOffThread<std::string>([](const std::string path)
{
auto profiler = fx::ResourceManager::GetCurrent(true)->GetComponent<fx::ProfilerComponent>();

if (profiler->IsRecording())
{
console::Printf("cmd", "Cannot save: profiler is active.\n");
return;
}

auto writeStream = MakeVfsStream(path);

if (!writeStream.GetRef())
{
return;
}

console::Printf("cmd", "Saving the recording as JSON to: %s.\n", path);

auto json = ConvertToJSON(ConvertToStorage(profiler));
auto jsonStr = json.dump(-1, ' ', false, nlohmann::detail::error_handler_t::replace);
writeStream->Write(jsonStr.data(), jsonStr.size());

console::Printf("cmd", "Save complete\n");
});

auto SaveToMsgPack = ExecuteOffThread<std::string>([](const std::string path)
{
auto profiler = fx::ResourceManager::GetCurrent(true)->GetComponent<fx::ProfilerComponent>();

if (profiler->IsRecording())
{
console::Printf("cmd", "Cannot save: profiler is active.\n");
return;
}

auto writeStream = profiler::MakeVfsStream(path);

if (!writeStream.GetRef())
{
return;
}

struct WriteWrapper
{
WriteWrapper(vfs::Stream& stream)
: stream(stream)
{
}

void write(const char* data, size_t size)
{
stream.Write(data, size);
}

vfs::Stream& stream;
} writeWrapper(*writeStream.GetRef());

console::Printf("cmd", "Saving the recording to: %s.\n", path);
msgpack::pack(writeWrapper, ConvertToStorage(profiler));
console::Printf("cmd", "Save complete\n");
});
}

// profiler help
// profiler status
Expand All @@ -440,6 +532,7 @@ namespace profilerCommand {
{"load", " <filename>"},
{"view", " [filename]" }
};


static fwRefContainer<console::Context> profilerCtx;
auto Setup()
Expand Down Expand Up @@ -573,83 +666,10 @@ namespace profilerCommand {
console::Printf("cmd", "Started recording\n");
}));

static auto makeVfsStream = [](const std::string& path) -> fwRefContainer<vfs::Stream>
{
std::string outFn = path;

#ifndef IS_FXSERVER
outFn = "citizen:/" + outFn;
#endif

auto vfsDevice = vfs::GetDevice(outFn);

if (!vfsDevice.GetRef())
{
console::PrintError("cmd", "Invalid path %s.\n", path);
return nullptr;
}

auto handle = vfsDevice->Create(outFn);

if (handle == vfs::Device::InvalidHandle)
{
console::PrintError("cmd", "Invalid path %s.\n", path);
return nullptr;
}

return new vfs::Stream(vfsDevice, handle);
};

static ConsoleCommand saveCmd(profilerCtx.GetRef(), "save", ExecuteOffThread<std::string>([](std::string path)
{
auto profiler = fx::ResourceManager::GetCurrent(true)->GetComponent<fx::ProfilerComponent>();

auto writeStream = makeVfsStream(path);

if (!writeStream.GetRef())
{
return;
}
static ConsoleCommand saveCmd(profilerCtx.GetRef(), "save", profiler::SaveToMsgPack);

struct WriteWrapper
{
WriteWrapper(vfs::Stream& stream)
: stream(stream)
{
}

void write(const char* data, size_t size)
{
stream.Write(data, size);
}

vfs::Stream& stream;
} writeWrapper(*writeStream.GetRef());

console::Printf("cmd", "Saving the recording to: %s.\n", path);
msgpack::pack(writeWrapper, ConvertToStorage(profiler));
console::Printf("cmd", "Save complete\n");
}));

static ConsoleCommand saveJSONCmd(profilerCtx.GetRef(), "saveJSON", ExecuteOffThread<std::string>([](std::string path)
{
auto writeStream = makeVfsStream(path);

if (!writeStream.GetRef())
{
return;
}

auto profiler = fx::ResourceManager::GetCurrent(true)->GetComponent<fx::ProfilerComponent>();

console::Printf("cmd", "Saving the recording as JSON to: %s.\n", path);

auto json = ConvertToJSON(ConvertToStorage(profiler));
auto jsonStr = json.dump(-1, ' ', false, nlohmann::detail::error_handler_t::replace);
writeStream->Write(reinterpret_cast<const uint8_t*>(jsonStr.data()), jsonStr.size());

console::Printf("cmd", "Save complete\n");
}));
static ConsoleCommand saveJSONCmd(profilerCtx.GetRef(), "saveJSON", profiler::SaveToJSON);

static ConsoleCommand dumpCmd(profilerCtx.GetRef(), "dump", ExecuteOffThread([]() {
auto profiler = fx::ResourceManager::GetCurrent(true)->GetComponent<fx::ProfilerComponent>();
Expand Down Expand Up @@ -931,6 +951,25 @@ namespace fx {

static InitFunction initFunction([]()
{
#ifndef IS_FXSERVER
// create the profiler directory
CreateDirectory(MakeRelativeCitPath(L"citizen/profiler").c_str(), nullptr);
// we should let the client decide if the client wants to allow the script to save profiles to their PC.
static ConVar<bool> allowProfilerSaveNatives("profiler_allowSaveNatives", ConVar_UserPref | ConVar_Archive, false);
#endif
auto CanProfilerSaveFiles = [&](const std::string& path) {
#ifndef IS_FXSERVER
if (!allowProfilerSaveNatives.GetValue())
{
console::PrintWarning("profiler", "Tried to save a profile to file: %s but the client did not have `profiler_allowSaveNatives` set to 'true'\n", path);
console::PrintWarning("profiler", "If you want to allow the profiler to save to your PC please do `profiler_allowSaveNatives true` in the console below.\n");
return false;
}
#endif

return true;
};

fx::ScriptEngine::RegisterNativeHandler("PROFILER_ENTER_SCOPE", [](fx::ScriptContext& ctx)
{
static auto profiler = fx::ResourceManager::GetCurrent()->GetComponent<fx::ProfilerComponent>();
Expand All @@ -949,6 +988,45 @@ static InitFunction initFunction([]()
ctx.SetResult<int>(profiler->IsRecording());
});

fx::ScriptEngine::RegisterNativeHandler("PROFILER_START_RECORDING", [](fx::ScriptContext& ctx)
{
static auto profiler = fx::ResourceManager::GetCurrent()->GetComponent<fx::ProfilerComponent>();
const int frames = ctx.GetArgument<int>(0);
const char* resource = ctx.GetArgument<char*>(1);
profiler->StartRecording(frames ? frames : -1, resource ? resource : "");
});

fx::ScriptEngine::RegisterNativeHandler("PROFILER_STOP_RECORDING", [](fx::ScriptContext& ctx)
{
static auto profiler = fx::ResourceManager::GetCurrent()->GetComponent<fx::ProfilerComponent>();
profiler->StopRecording();
});

fx::ScriptEngine::RegisterNativeHandler("PROFILER_SAVE_TO_JSON", [CanProfilerSaveFiles](fx::ScriptContext& ctx)
{
const std::string path = ctx.CheckArgument<const char*>(0);

if (!CanProfilerSaveFiles(path))
{
return;
}

profiler::SaveToJSON(path);
});

fx::ScriptEngine::RegisterNativeHandler("PROFILER_SAVE_TO_MSGPACK", [CanProfilerSaveFiles](fx::ScriptContext& ctx)
{
const std::string path = ctx.CheckArgument<const char*>(0);

if (!CanProfilerSaveFiles(path))
{
return;
}


profiler::SaveToMsgPack(path);
});

fx::Resource::OnInitializeInstance.Connect([](fx::Resource* res)
{
auto resname = res->GetName();
Expand Down
17 changes: 17 additions & 0 deletions ext/native-decls/ProfilerSaveToJson.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
ns: CFX
apiset: shared
---
## PROFILER_SAVE_TO_JSON

```c
void PROFILER_SAVE_TO_JSON(char* fileName);
```
Saves the current profile in JSON format
On the server `fileName` will be the absolute path of where to save the profiler record to, like `C:\FiveM_Profilers`
On the client `fileName` will be the path relative to the `citizen/profiler` folder, by default this is `%localappdata%/FiveM/FiveM.app/citizen/profiler`
## Parameters
* **fileName**: The file name to save to, this should include the file extension, please see notes above.
17 changes: 17 additions & 0 deletions ext/native-decls/ProfilerSaveToMsgpack.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
ns: CFX
apiset: shared
---
## PROFILER_SAVE_TO_MSGPACK

```c
void PROFILER_SAVE_TO_MSGPACK(char* fileName);
```
Saves the current profile in message pack format
On the server `fileName` will be the absolute path of where to save the profiler record to, like `C:\FiveM_Profilers`
On the client `fileName` will be the path relative to the `citizen/profiler` folder, by default this is `%localappdata%/FiveM/FiveM.app/citizen/profiler`
## Parameters
* **fileName**: The file name to save to, this should include file extensions, please see notes above.
15 changes: 15 additions & 0 deletions ext/native-decls/ProfilerStartRecording.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
ns: CFX
apiset: shared
---
## PROFILER_START_RECORDING

```c
void PROFILER_START_RECORDING(int frames, char* resourceName);
```
Starts recording on the profiler.
## Parameters
* **frames**: The amount of frames to record for, -1 to record until calling [PROFILER_STOP_RECORDING](#_0x2d29dea5)
* **resourceName**: The resource to record for, or null to record every resource.
11 changes: 11 additions & 0 deletions ext/native-decls/ProfilerStopRecording.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
ns: CFX
apiset: shared
---
## PROFILER_STOP_RECORDING

```c
void PROFILER_STOP_RECORDING();
```

Stops the profiler if its currently recording.

0 comments on commit 6dad489

Please sign in to comment.