Skip to content
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
124 changes: 124 additions & 0 deletions .cognition/skills/profile-perlonjava/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
# Profile PerlOnJava

Profile and optimize PerlOnJava runtime performance using Java Flight Recorder.

## When to Use

- Investigating performance bottlenecks in Perl scripts running on PerlOnJava
- Finding optimization opportunities in the runtime
- Measuring impact of optimizations

## Workflow

### 1. Run with JFR Profiling

```bash
cd /Users/fglock/projects/PerlOnJava2

# Profile a long-running script (adjust duration as needed)
java -XX:+FlightRecorder \
-XX:StartFlightRecording=duration=60s,filename=profile.jfr \
-jar target/perlonjava-3.0.0.jar <script.pl> [args...]
```

### 2. Analyze with JFR Tools

```bash
# Path to jfr tool
JFR="$(/usr/libexec/java_home)/bin/jfr"

# Summary of recorded events
$JFR summary profile.jfr

# Extract execution samples (CPU hotspots)
$JFR print --events jdk.ExecutionSample profile.jfr

# Aggregate hotspots by method (most useful)
$JFR print --events jdk.ExecutionSample profile.jfr 2>&1 | \
grep -E "^\s+[a-z].*line:" | \
sed 's/line:.*//' | \
sort | uniq -c | sort -rn | head -40
```

### 3. Key Hotspot Categories

| Category | Methods to Watch | Optimization Approach |
|----------|------------------|----------------------|
| **Number parsing** | `Long.parseLong`, `Double.parseDouble`, `NumberParser.parseNumber` | Cache numeric values, avoid string→number conversions |
| **Type checking** | `ScalarUtils.looksLikeNumber`, `RuntimeScalar.getDefinedBoolean` | Fast-path for common types (INTEGER, DOUBLE) |
| **Bitwise ops** | `BitwiseOperators.*` | Ensure values stay as INTEGER type |
| **Regex** | `Pattern.match`, `Matcher.matches` | Reduce unnecessary regex checks |
| **Loop control** | `RuntimeControlFlowRegistry.checkLoopAndGetAction` | ThreadLocal overhead |
| **Array ops** | `ArrayList.grow`, `Arrays.copyOf` | Pre-size arrays, reduce allocations |

### 4. Common Runtime Files

| File | Purpose |
|------|---------|
| `src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalar.java` | Scalar value representation, getLong/getDouble/getInt |
| `src/main/java/org/perlonjava/runtime/runtimetypes/ScalarUtils.java` | Utility functions like looksLikeNumber |
| `src/main/java/org/perlonjava/runtime/operators/BitwiseOperators.java` | Bitwise operations (&, |, ^, ~, <<, >>) |
| `src/main/java/org/perlonjava/runtime/operators/Operator.java` | General operators |
| `src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeArray.java` | Array operations |

### 5. Optimization Patterns

#### Fast-path for common types
```java
public static boolean looksLikeNumber(RuntimeScalar runtimeScalar) {
// Inlined fast-path for most common numeric types
int t = runtimeScalar.type;
if (t == INTEGER || t == DOUBLE) {
return true;
}
return looksLikeNumberSlow(runtimeScalar, t);
}
```

#### Avoid repeated parsing
```java
// Bad: parses string every time
long val = runtimeScalar.getLong(); // calls Long.parseLong if STRING

// Better: check type first, use cached value
if (runtimeScalar.type == INTEGER) {
long val = (int) runtimeScalar.value; // direct access
}
```

### 6. Benchmark Commands

```bash
# Quick benchmark with life_bitpacked.pl
java -jar target/perlonjava-3.0.0.jar examples/life_bitpacked.pl \
-w 200 -h 200 -g 10000 -r none

# Multiple runs for consistency
for i in 1 2 3; do
java -jar target/perlonjava-3.0.0.jar examples/life_bitpacked.pl \
-w 200 -h 200 -g 10000 -r none 2>&1 | grep "per second"
done
```

### 7. Build and Test

```bash
# Rebuild after changes
mvn package -q -DskipTests

# Run tests to verify correctness
mvn test -q
```

## Example Session

```
1. Identify slow script or operation
2. Profile with JFR (60s recording)
3. Aggregate hotspots by method
4. Identify top bottlenecks (parsing, type checks, etc.)
5. Implement fast-path optimization
6. Rebuild and benchmark
7. Profile again to verify improvement
8. Run tests to ensure correctness
```
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,4 @@ Image-ExifTool-*

# Ignore xxx/ directory (temporary module staging area)
xxx/
*.jfr
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,20 @@ public class RuntimeScalar extends RuntimeBase implements RuntimeScalarReference
// Static stack to store saved "local" states of RuntimeScalar instances
private static final Stack<RuntimeScalar> dynamicStateStack = new Stack<>();

// Pre-compiled regex patterns for numification fast-paths
// These are used to avoid StackOverflowError from repeated Pattern.compile() calls
private static final Pattern INTEGER_PATTERN = Pattern.compile("^-?\\d+$");
// Pre-compiled regex pattern for decimal numification fast-path
// INTEGER_PATTERN replaced with isIntegerString() for better performance
private static final Pattern DECIMAL_PATTERN = Pattern.compile("^[+-]?(?:\\d+(?:\\.\\d*)?|\\.\\d+)(?:[eE][+-]?\\d+)?$");

// Fast check if string might be a parseable integer
// Returns true if first char suggests it could be an integer (digit or minus)
// This avoids exception overhead for strings like "hello" while allowing
// Long.parseLong to handle edge cases like overflow
private static boolean mightBeInteger(String s) {
if (s.isEmpty()) return false;
char c = s.charAt(0);
return (c >= '0' && c <= '9') || c == '-';
}

// Type map for scalar types to their corresponding enum
private static final Map<Class<?>, Integer> typeMap = new HashMap<>();

Expand Down Expand Up @@ -327,14 +336,11 @@ private int getIntLarge() {
String s = (String) value;
if (s != null) {
String t = s.trim();
if (!t.isEmpty() && INTEGER_PATTERN.matcher(t).matches()) {
if (mightBeInteger(t)) {
try {
// Parse as long first so we can handle values outside 32-bit range
// (Perl IV is commonly 64-bit). getInt() is used for array indices
// and similar contexts, which should behave like (int)getLong().
yield (int) Long.parseLong(t);
} catch (NumberFormatException ignored) {
// Fall through to full numification.
// Fall through to full numification (handles "1.5", overflow, etc.)
}
}
}
Expand Down Expand Up @@ -501,11 +507,11 @@ public long getLong() {
String s = (String) value;
if (s != null) {
String t = s.trim();
if (!t.isEmpty() && INTEGER_PATTERN.matcher(t).matches()) {
if (mightBeInteger(t)) {
try {
yield Long.parseLong(t);
} catch (NumberFormatException ignored) {
// Fall through to full numification.
// Fall through to full numification (handles "1.5", overflow, etc.)
}
}
}
Expand Down
56 changes: 33 additions & 23 deletions src/main/java/org/perlonjava/runtime/runtimetypes/ScalarUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -137,33 +137,43 @@ public static boolean isInteger(String str) {
}

public static boolean looksLikeNumber(RuntimeScalar runtimeScalar) {
switch (runtimeScalar.type) {
case INTEGER:
case DOUBLE:
// Inlined fast-path for most common numeric types (helps JIT inlining)
int t = runtimeScalar.type;
if (t == INTEGER || t == DOUBLE) {
return true;
}
return looksLikeNumberSlow(runtimeScalar, t);
}

// Slow path for looksLikeNumber - handles strings and other types
private static boolean looksLikeNumberSlow(RuntimeScalar runtimeScalar, int t) {
if (t == STRING || t == BYTE_STRING || t == VSTRING) {
String str = runtimeScalar.toString().trim();
if (str.isEmpty()) {
return false;
}
// Check for Inf and NaN
if (str.equalsIgnoreCase("Inf") || str.equalsIgnoreCase("Infinity") || str.equalsIgnoreCase("NaN")) {
return true;
case STRING, BYTE_STRING, VSTRING:
String str = runtimeScalar.toString().trim();
if (str.isEmpty()) {
return false;
}
// Check for Inf and NaN
if (str.equalsIgnoreCase("Inf") || str.equalsIgnoreCase("Infinity") || str.equalsIgnoreCase("NaN")) {
return true;
}
// Check for decimal (integer or float)
try {
Double.parseDouble(str);
return true;
} catch (NumberFormatException e) {
return false;
}
case BOOLEAN, DUALVAR:
}
// Fast check: if first char isn't digit, +, -, or . it's not a number
char first = str.charAt(0);
if (!((first >= '0' && first <= '9') || first == '+' || first == '-' || first == '.')) {
return false;
}
// Check for decimal (integer or float)
try {
Double.parseDouble(str);
return true;
case TIED_SCALAR:
return looksLikeNumber(runtimeScalar.tiedFetch());
default:
} catch (NumberFormatException e) {
return false;
}
} else if (t == BOOLEAN || t == DUALVAR) {
return true;
} else if (t == TIED_SCALAR) {
return looksLikeNumber(runtimeScalar.tiedFetch());
}
return false;
}

/**
Expand Down