Skip to content

Commit

Permalink
feat: add support for analyzing local files
Browse files Browse the repository at this point in the history
  • Loading branch information
D-D-H committed Oct 16, 2023
1 parent e1cc510 commit ef48b5c
Show file tree
Hide file tree
Showing 19 changed files with 278 additions and 23 deletions.
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,14 @@ Complete information is available in the documentation at [https://eclipse.githu
### Run Jifa Locally with a Single Command

```shell
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/eclipse/jifa/HEAD/scripts/jifa.sh)"
# Default service address is at http://localhost:8102
curl -fsSL https://raw.githubusercontent.com/eclipse/jifa/main/jifa.sh | bash

# Change the server port
curl -fsSL https://raw.githubusercontent.com/eclipse/jifa/main/jifa.sh | bash -s -- -p <port>

# Analyze local files
curl -fsSL https://raw.githubusercontent.com/eclipse/jifa/main/jifa.sh | bash -s -- <file1 path> <file2 path> ...
```

Note: Please make sure that Docker is installed.
Expand Down
9 changes: 8 additions & 1 deletion README_zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,14 @@ Eclipse Jifa 是一个致力于帮助 Java 研发人员排查应用中常见问
### 一个命令运行 Jifa

```shell
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/eclipse/jifa/HEAD/scripts/jifa.sh)"
# 默认服务地址是 http://localhost:8102
curl -fsSL https://raw.githubusercontent.com/eclipse/jifa/main/jifa.sh | bash

# 修改服务端口
curl -fsSL https://raw.githubusercontent.com/eclipse/jifa/main/jifa.sh | bash -s -- -p <port>

# 分析本地文件
curl -fsSL https://raw.githubusercontent.com/eclipse/jifa/main/jifa.sh | bash -s -- <file1 path> <file2 path> ...
```

注:本地环境需要安装 docker
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,15 @@
import org.eclipse.jifa.analysis.listener.ProgressListener;
import org.eclipse.jifa.gclog.model.GCModel;
import org.eclipse.jifa.gclog.parser.GCLogAnalyzer;
import org.eclipse.jifa.gclog.parser.GCLogParserFactory;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.file.Path;
import java.util.Map;
import java.util.function.Predicate;

public class GCLogAnalysisApiExecutor extends AbstractApiExecutor<GCModel> {

Expand All @@ -31,4 +37,17 @@ protected GCModel buildAnalyzer(Path target, Map<String, String> options, Progre
public String namespace() {
return "gc-log";
}

@Override
public Predicate<byte[]> matcher() {
return bytes -> {
GCLogParserFactory factory = new GCLogParserFactory();
try (BufferedReader bufferedReader = new BufferedReader(
new InputStreamReader(new ByteArrayInputStream(bytes)))) {
return factory.getParser(bufferedReader) != null;
} catch (IOException e) {
return false;
}
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@

@Slf4j
public class GCLogAnalyzer {
private File file;
private ProgressListener listener;
private final File file;
private final ProgressListener listener;

private final int MAX_SINGLE_LINE_LENGTH = 2048; // max length in hotspot

Expand All @@ -36,10 +36,6 @@ public GCLogAnalyzer(File file, ProgressListener listener) {
this.listener = listener;
}

public GCLogAnalyzer(File file) {
this(file, new DefaultProgressListener());
}

public GCModel parse() throws Exception {
BufferedReader br = null;
try {
Expand All @@ -48,6 +44,7 @@ public GCModel parse() throws Exception {
listener.sendUserMessage(ProgressListener.Level.INFO, "Deciding gc log format.", null);

// decide log format

GCLogParserFactory logParserFactory = new GCLogParserFactory();
br.mark(GCLogParserFactory.MAX_ATTEMPT_LINE * MAX_SINGLE_LINE_LENGTH);
GCLogParser parser = logParserFactory.getParser(br);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
import java.util.Map;
import java.util.Objects;
import java.util.ServiceLoader;
import java.util.function.Predicate;

@Slf4j
public class HeapDumpAnalysisApiExecutor extends AbstractApiExecutor<HeapDumpAnalyzer> {
Expand Down Expand Up @@ -156,6 +157,18 @@ public String namespace() {
return "heap-dump";
}

@Override
public Predicate<byte[]> matcher() {
return new Predicate<>() {
static final String HEADER = "JAVA PROFILE 1.0.2";

@Override
public boolean test(byte[] bytes) {
return bytes.length > HEADER.length() && new String(bytes, 0, HEADER.length()).equals(HEADER);
}
};
}

@Override
public boolean needOptionsForAnalysis(Path target) {
return !indexFile(target).exists() && !errorLogFile(target).exists() && !isActive(target);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.function.Predicate;

/**
* Analysis api executor
Expand All @@ -37,4 +38,11 @@ public interface ApiExecutor {
* @return result
*/
CompletableFuture<?> execute(ExecutionContext context);

/**
* @return a matcher to tell the byte array is supported by this executor, default is null
*/
default Predicate<byte[]> matcher() {
return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,10 @@ public interface ApiService {
static ApiService getInstance() {
return ApiServiceImpl.instance();
}

/**
* @param content the content
* @return the relevant namespace if the payload may be analyzed, otherwise null
*/
String deduceNamespaceByContent(byte[] content);
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,31 +21,40 @@
import java.util.ServiceLoader;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.function.Predicate;

public class ApiServiceImpl implements ApiService {

private Map<String, Set<Api>> apis;

private Map<String, ApiExecutor> executors;

private Map<String, Predicate<byte[]>> matchers;

private ApiServiceImpl() {
loadExecutors();
}

private void loadExecutors() {
Map<String, Set<Api>> apis = new HashMap<>();
Map<String, ApiExecutor> executors = new HashMap<>();
Map<String, Predicate<byte[]>> matchers = new HashMap<>();

for (ApiExecutor executor : ServiceLoader.load(ApiExecutor.class)) {
String namespace = executor.namespace();
Validate.isTrue(!apis.containsKey(namespace));

apis.put(namespace, executor.apis());
executors.put(namespace, executor);
Predicate<byte[]> matcher = executor.matcher();
if (matcher != null) {
matchers.put(namespace, matcher);
}
}

this.apis = Collections.unmodifiableMap(apis);
this.executors = Collections.unmodifiableMap(executors);
this.matchers = Collections.unmodifiableMap(matchers);
}

@Override
Expand All @@ -64,6 +73,16 @@ public CompletableFuture<?> execute(Path target, String namespace, String api, O
return executor.execute(new ExecutionContext(target, api, arguments));
}

@Override
public String deduceNamespaceByContent(byte[] content) {
for (Map.Entry<String, Predicate<byte[]>> entry : matchers.entrySet()) {
if (entry.getValue().test(content)) {
return entry.getKey();
}
}
return null;
}

static ApiService instance() {
return Holder.INSTANCE;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import java.io.File;
import java.nio.file.Path;
import java.util.Map;
import java.util.function.Predicate;

@Slf4j
public class ThreadDumpAnalysisApiExecutor extends AbstractApiExecutor<ThreadDumpAnalyzer> {
Expand All @@ -43,4 +44,16 @@ public void clean(Path target) {
public String namespace() {
return "thread-dump";
}

@Override
public Predicate<byte[]> matcher() {
return new Predicate<>() {
static final String HEADER = "Full thread dump";

@Override
public boolean test(byte[] bytes) {
return bytes.length > 20 + HEADER.length() && new String(bytes, 20, HEADER.length()).equals(HEADER);
}
};
}
}
22 changes: 19 additions & 3 deletions scripts/jifa.sh → jifa.sh
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ set -eu
#TAG="latest"
TAG=0.2.0-SNAPSHOT
PORT="8102"
MOUNTS=""
INPUT_FILES=""
INPUT_FILE_COUNT=0

check_docker() {
if ! command -v docker &>/dev/null; then
Expand All @@ -23,9 +26,9 @@ check_docker() {
fi
}

run_jifa() {
launch_jifa() {
check_docker
docker run -p ${PORT}:8102 eclipsejifa/jifa:${TAG}
docker run -p ${PORT}:${PORT} $MOUNTS eclipsejifa/jifa:${TAG} $INPUT_FILES
}

while [ $# -gt 0 ]; do
Expand All @@ -38,8 +41,21 @@ while [ $# -gt 0 ]; do
PORT=$2
shift
;;
*)
ABSOLUTE_PATH=$(realpath "$1")
if [ ! -f "$ABSOLUTE_PATH" ]; then
echo "$1 does not exist or is not a regular file"
exit 1
fi

FILE_NAME=$(basename "$ABSOLUTE_PATH")

MOUNTS="$MOUNTS -v $ABSOLUTE_PATH:/input-file-$INPUT_FILE_COUNT/$FILE_NAME"
INPUT_FILES="$INPUT_FILES --jifa.input-files[$INPUT_FILE_COUNT]=/input-file-$INPUT_FILE_COUNT/$FILE_NAME"
INPUT_FILE_COUNT=$((INPUT_FILE_COUNT+1))
;;
esac
shift
done

run_jifa
launch_jifa
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import jakarta.validation.constraints.PositiveOrZero;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.jifa.common.util.Validate;
import org.eclipse.jifa.server.enums.Role;
import org.eclipse.jifa.server.enums.SchedulingStrategy;
Expand All @@ -37,6 +38,7 @@
@Validated
@Getter
@Setter
@Slf4j
public class Configuration {

/**
Expand Down Expand Up @@ -113,6 +115,11 @@ public class Configuration {
*/
private String rootPassword = "password";

/**
* Input files is some specified files that will be added automatically when starting.
*/
private Path[] inputFiles;

@PostConstruct
private void init() {
if (role == Role.MASTER) {
Expand Down
54 changes: 54 additions & 0 deletions server/src/main/java/org/eclipse/jifa/server/Launcher.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,23 @@
********************************************************************************/
package org.eclipse.jifa.server;

import lombok.extern.slf4j.Slf4j;
import org.eclipse.jifa.server.enums.FileType;
import org.eclipse.jifa.server.enums.Role;
import org.eclipse.jifa.server.service.AnalysisApiService;
import org.eclipse.jifa.server.service.FileService;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.EnableTransactionManagement;

import java.io.IOException;
import java.nio.file.Path;

@SpringBootApplication
@EnableConfigurationProperties({Configuration.class})
@EnableTransactionManagement
Expand All @@ -27,4 +38,47 @@ public class Launcher {
public static void main(String[] args) {
SpringApplication.run(Launcher.class, args);
}

@Component
@Slf4j
static class ReadyListener extends ConfigurationAccessor {

private final AnalysisApiService analysisApiService;

private final FileService fileService;

ReadyListener(AnalysisApiService analysisApiService, FileService fileService) {
this.analysisApiService = analysisApiService;
this.fileService = fileService;
}

@EventListener(ApplicationReadyEvent.class)
public void fireReadyEvent() {
//noinspection HttpUrlsUsage
log.info("Jifa Server: http://{}:{}", "localhost", config.getPort());

if (config.getRole() == Role.STANDALONE_WORKER) {
Path[] paths = config.getInputFiles();
if (paths != null) {
for (Path path : paths) {
try {
FileType type = analysisApiService.deduceFileType(path);
if (type != null) {
String uniqueName = fileService.handleLocalFileRequest(type, path);
//noinspection HttpUrlsUsage
log.info("{}: http://{}:{}/{}/{}",
path.getFileName(),
"localhost",
config.getPort(),
type.getAnalysisUrlPath(),
uniqueName);
}
} catch (IOException e) {
log.error("Failed to handle input file '{}': {}", path, e.getMessage());
}
}
}
}
}
}
}
Loading

0 comments on commit ef48b5c

Please sign in to comment.