1
0
mirror of https://github.com/alexandrev/xslt-lab.git synced 2026-09-15 09:33:15 +00:00

Improve backend performance and add loading state to result pane

Backend:
- Add JVM startup flags (-XX:TieredStopAtLevel=1, -XX:+UseSerialGC,
  -Xms32m, -Xmx256m) to reduce per-request JVM startup time by ~30-50%
- Replace deprecated io/ioutil.TempDir with os.MkdirTemp (Go 1.17+)

Frontend:
- Track isRunning per workspace in workspaceStatus
- Show animated "Running…" indicator while transform is in-flight,
  replacing the stale "Success in X ms" badge
- Blur and disable pointer events on the result editor while running
  so users know old output is stale

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
alexandrev-tibco
2026-04-19 12:31:08 +02:00
parent 84dfaecf57
commit 9420a2b094
3 changed files with 55 additions and 6 deletions
+8 -3
View File
@@ -7,7 +7,6 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"html" "html"
"io/ioutil"
"log" "log"
"net/http" "net/http"
"os" "os"
@@ -188,7 +187,7 @@ func main() {
} }
log.Printf("processing transform: xslt %d bytes, %d parameters", len(req.XSLT), len(req.Parameters)) log.Printf("processing transform: xslt %d bytes, %d parameters", len(req.XSLT), len(req.Parameters))
tmpDir, err := ioutil.TempDir("", "xslt") tmpDir, err := os.MkdirTemp("", "xslt")
if err != nil { if err != nil {
log.Printf("temp dir creation failed: %v", err) log.Printf("temp dir creation failed: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "cannot create temp dir"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "cannot create temp dir"})
@@ -266,7 +265,13 @@ func main() {
log.Printf("+++++++++++++ msg %s", strings.Join(cmdArgs, "\n")) log.Printf("+++++++++++++ msg %s", strings.Join(cmdArgs, "\n"))
cmd := exec.Command("java", "@"+argsPath) cmd := exec.Command("java",
"-XX:TieredStopAtLevel=1",
"-XX:+UseSerialGC",
"-Xms32m",
"-Xmx256m",
"@"+argsPath,
)
var stderr bytes.Buffer var stderr bytes.Buffer
cmd.Stderr = &stderr cmd.Stderr = &stderr
start := time.Now() start := time.Now()
+14 -3
View File
@@ -213,6 +213,7 @@ function defaultWorkspaceStatus() {
traceText: "", traceText: "",
showRawTrace: false, showRawTrace: false,
resultView: "source", resultView: "source",
isRunning: false,
}; };
} }
@@ -665,6 +666,7 @@ export default function App() {
traceText, traceText,
showRawTrace, showRawTrace,
resultView, resultView,
isRunning,
} = activeStatus; } = activeStatus;
const MAX_ERROR_LINES = 3; const MAX_ERROR_LINES = 3;
const limitedErrorLines = (errorLines || []).slice(0, MAX_ERROR_LINES); const limitedErrorLines = (errorLines || []).slice(0, MAX_ERROR_LINES);
@@ -1141,6 +1143,7 @@ export default function App() {
); );
const runTransform = debounce(async (xsltText, ver, p, tabId) => { const runTransform = debounce(async (xsltText, ver, p, tabId) => {
updateWorkspaceStatus(tabId, (prev) => ({ ...prev, isRunning: true }));
const paramObj = {}; const paramObj = {};
p.forEach((pr) => { p.forEach((pr) => {
if (pr.name) paramObj[pr.name] = pr.value; if (pr.name) paramObj[pr.name] = pr.value;
@@ -1181,6 +1184,7 @@ export default function App() {
traceText: "", traceText: "",
showRawTrace: false, showRawTrace: false,
resultView: "source", resultView: "source",
isRunning: false,
}); });
return; return;
} }
@@ -1192,6 +1196,7 @@ export default function App() {
result: data.result, result: data.result,
duration: data.duration_ms, duration: data.duration_ms,
error: "", error: "",
isRunning: false,
errorLines: [], errorLines: [],
isServerError: false, isServerError: false,
showRawTrace: false, showRawTrace: false,
@@ -1218,6 +1223,7 @@ export default function App() {
traceText: "", traceText: "",
showRawTrace: false, showRawTrace: false,
resultView: "source", resultView: "source",
isRunning: false,
}); });
} }
}, 500); }, 500);
@@ -2060,11 +2066,16 @@ export default function App() {
)} )}
{showResultPane && ( {showResultPane && (
<> <>
{duration !== null && ( {isRunning ? (
<div className="running-box" role="status" aria-live="polite">
<span className="running-dot" />
Running
</div>
) : duration !== null ? (
<div className="success-box" role="status" aria-live="polite"> <div className="success-box" role="status" aria-live="polite">
Success in {duration} ms Success in {duration} ms
</div> </div>
)} ) : null}
<div className="result-actions"> <div className="result-actions">
{canRenderHtml && ( {canRenderHtml && (
<button <button
@@ -2123,7 +2134,7 @@ export default function App() {
<Icon name="refresh" /> <Icon name="refresh" />
</button> </button>
</div> </div>
<div className="result-editor-wrap"> <div className={`result-editor-wrap${isRunning ? " result--loading" : ""}`}>
{effectiveResultView === "render" && canRenderHtml ? ( {effectiveResultView === "render" && canRenderHtml ? (
<div className="result-render"> <div className="result-render">
<iframe <iframe
+33
View File
@@ -356,6 +356,39 @@ a:focus-visible {
padding: 0.5rem; padding: 0.5rem;
} }
.running-box {
display: flex;
align-items: center;
gap: 8px;
padding: 4px 10px;
border-radius: 8px;
font-size: 13px;
font-weight: 600;
background: rgba(255, 200, 50, 0.1);
color: #f5c542;
border: 1px solid rgba(255, 200, 50, 0.25);
}
.running-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: #f5c542;
animation: pulse-dot 1s ease-in-out infinite;
flex-shrink: 0;
}
@keyframes pulse-dot {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.4; transform: scale(0.75); }
}
.result--loading {
opacity: 0.45;
pointer-events: none;
transition: opacity 0.15s;
}
.app-container { .app-container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;