Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for running the production client with the tracy profiler. #1244

Merged
merged 3 commits into from
Jan 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,17 @@
package net.fabricmc.loom.task.prod;

import java.io.File;
import java.io.IOException;

import javax.inject.Inject;

import org.gradle.api.Action;
import org.gradle.api.file.DirectoryProperty;
import org.gradle.api.provider.Property;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.InputFiles;
import org.gradle.api.tasks.Nested;
import org.gradle.api.tasks.Optional;
import org.gradle.process.ExecSpec;
import org.jetbrains.annotations.ApiStatus;

Expand All @@ -53,6 +57,21 @@ public abstract non-sealed class ClientProductionRunTask extends AbstractProduct
@Input
public abstract Property<Boolean> getUseXVFB();

@Nested
@Optional
public abstract Property<TracyCapture> getTracyCapture();

/**
* Configures the tracy profiler to run alongside the game. See @{@link TracyCapture} for more information.
*
* @param action The configuration action.
*/
public void tracy(Action<? super TracyCapture> action) {
getTracyCapture().set(getProject().getObjects().newInstance(TracyCapture.class));
getTracyCapture().finalizeValue();
action.execute(getTracyCapture().get());
}

// Internal options
@Input
protected abstract Property<String> getAssetsIndex();
Expand Down Expand Up @@ -86,6 +105,16 @@ public ClientProductionRunTask() {
dependsOn("downloadAssets");
}

@Override
public void run() throws IOException {
if (getTracyCapture().isPresent()) {
getTracyCapture().get().runWithTracy(super::run);
return;
}

super.run();
}

@Override
protected void configureCommand(ExecSpec exec) {
if (getUseXVFB().get()) {
Expand Down Expand Up @@ -120,5 +149,9 @@ protected void configureProgramArgs(ExecSpec exec) {
"--assetsDir", getAssetsDir().get().getAsFile().getAbsolutePath(),
"--gameDir", getRunDir().get().getAsFile().getAbsolutePath()
);

if (getTracyCapture().isPresent()) {
exec.args("--tracy");
}
}
}
159 changes: 159 additions & 0 deletions src/main/java/net/fabricmc/loom/task/prod/TracyCapture.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
/*
* This file is part of fabric-loom, licensed under the MIT License (MIT).
*
* Copyright (c) 2025 FabricMC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/

package net.fabricmc.loom.task.prod;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.function.Consumer;

import javax.inject.Inject;

import org.gradle.api.file.RegularFileProperty;
import org.gradle.api.provider.Property;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.InputFile;
import org.gradle.api.tasks.Optional;
import org.gradle.api.tasks.OutputFile;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import net.fabricmc.loom.util.ExceptionUtil;

public abstract class TracyCapture {
private static final Logger LOGGER = LoggerFactory.getLogger(TracyCapture.class);

/**
* The path to the tracy-capture executable.
*/
@InputFile
@Optional
public abstract RegularFileProperty getTracyCapture();

/**
* The maximum number of seconds to wait for tracy-capture to stop on its own before killing it.
*
* <p>Defaults to 10 seconds.
*/
@Input
public abstract Property<Integer> getMaxShutdownWaitSeconds();

/**
* The path to the output file.
*/
@OutputFile
@Optional
public abstract RegularFileProperty getOutput();

@Inject
public TracyCapture() {
getMaxShutdownWaitSeconds().convention(10);
}

void runWithTracy(IORunnable runnable) throws IOException {
TracyCaptureRunner tracyCaptureRunner = createRunner();

boolean success = false;

try {
runnable.run();
success = true;
} finally {
try {
tracyCaptureRunner.close();
} catch (Exception e) {
if (success) {
//noinspection ThrowFromFinallyBlock
throw ExceptionUtil.createDescriptiveWrapper(RuntimeException::new, "Failed to stop tracy capture", e);
}
}
}
}

private TracyCaptureRunner createRunner() throws IOException {
File tracyCapture = getTracyCapture().getAsFile().get();
File output = getOutput().getAsFile().get();

ProcessBuilder builder = new ProcessBuilder()
.command(tracyCapture.getAbsolutePath(), "-a", "127.0.0.1", "-f", "-o", output.getAbsolutePath());
Process process = builder.start();

captureLog(process.getInputStream(), LOGGER::info);
captureLog(process.getErrorStream(), LOGGER::error);

LOGGER.info("Tracy capture started");

return new TracyCaptureRunner(process, getMaxShutdownWaitSeconds().get());
}

private record TracyCaptureRunner(Process process, int shutdownWait) implements AutoCloseable {
@Override
public void close() throws Exception {
// Wait x seconds for tracy to stop on its own
// This allows time for tracy to save the profile to disk
for (int i = 0; i < shutdownWait; i++) {
if (!process.isAlive()) {
break;
}

try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}

// If it's still running, kill it
if (process.isAlive()) {
LOGGER.error("Tracy capture did not stop on its own, killing it");
process.destroy();
process.waitFor();
}

int exitCode = process.exitValue();

if (exitCode != 0) {
throw new RuntimeException("Tracy capture failed with exit code " + exitCode);
}
}
}

private static void captureLog(InputStream inputStream, Consumer<String> lineConsumer) {
new Thread(() -> {
try {
new BufferedReader(new InputStreamReader(inputStream)).lines().forEach(lineConsumer);
} catch (Exception e) {
// Don't really care, this will happen when the stream is closed
}
}).start();
}

@FunctionalInterface
public interface IORunnable {
void run() throws IOException;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,20 +32,24 @@ import spock.lang.Timeout
import spock.lang.Unroll
import spock.util.environment.RestoreSystemProperties

import net.fabricmc.loom.test.LoomTestConstants
import net.fabricmc.loom.test.util.GradleProjectTestTrait
import net.fabricmc.loom.util.download.Download

import static net.fabricmc.loom.test.LoomTestConstants.STANDARD_TEST_VERSIONS
import static org.gradle.testkit.runner.TaskOutcome.SUCCESS

// This test runs a mod that exits on mod init
class RunConfigTest extends Specification implements GradleProjectTestTrait {
private static List<String> tasks = [
private static final List<String> tasks = [
"runClient",
"runServer",
"runTestmodClient",
"runTestmodServer",
"runAutoTestServer"
]
private static final String TRACY_CAPTURE_LINUX = "https://github.com/modmuss50/tracy-utils/releases/download/0.0.2/linux-x86_64-tracy-capture"

@Unroll
def "Run config #task (gradle #version)"() {
setup:
Expand Down Expand Up @@ -165,6 +169,9 @@ class RunConfigTest extends Specification implements GradleProjectTestTrait {
@IgnoreIf({ !os.linux }) // XVFB is installed on the CI for this test
def "prod client (gradle #version)"() {
setup:
def tracyCapture = new File(LoomTestConstants.TEST_DIR, "tracy-capture")
Download.create(TRACY_CAPTURE_LINUX).defaultCache().downloadPath(tracyCapture.toPath())

def gradle = gradleProject(project: "minimalBase", version: version)
gradle.buildGradle << '''
configurations {
Expand All @@ -183,13 +190,25 @@ class RunConfigTest extends Specification implements GradleProjectTestTrait {
tasks.register("prodClient", net.fabricmc.loom.task.prod.ClientProductionRunTask) {
mods.from(configurations.productionMods)
jvmArgs.add("-Dfabric.client.gametest")

tracy {
tracyCapture = file("tracy-capture")
output = file("profile.tracy")
}
}
'''

// Copy tracy into the project
def projectTracyCapture = new File(gradle.projectDir, "tracy-capture")
projectTracyCapture.bytes = tracyCapture.bytes
projectTracyCapture.setExecutable(true)

when:
def result = gradle.run(task: "prodClient")

then:
result.task(":prodClient").outcome == SUCCESS
new File(gradle.projectDir, "profile.tracy").exists()

where:
version << STANDARD_TEST_VERSIONS
Expand Down