Skip to content

Commit

Permalink
Add support for running the production client with the tracy profiler. (
Browse files Browse the repository at this point in the history
#1244)

* Add support for running the production client with the tracy profiler.

* Fix test

* Update tracy capture
  • Loading branch information
modmuss50 authored Jan 4, 2025
1 parent 52a19b3 commit e1cc6f0
Show file tree
Hide file tree
Showing 3 changed files with 212 additions and 1 deletion.
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

0 comments on commit e1cc6f0

Please sign in to comment.