Skip to content

Commit

Permalink
Optimize the TonY AM web dashboard page (#667)
Browse files Browse the repository at this point in the history
Signed-off-by: zhangjunfan <[email protected]>
  • Loading branch information
zuston authored May 22, 2022
1 parent 6e0985b commit 7e2e8a5
Show file tree
Hide file tree
Showing 8 changed files with 12,169 additions and 88 deletions.
3 changes: 2 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,8 @@ ext.deps = [
"sshd": "org.apache.sshd:sshd-core:1.1.0",
"testng": "org.testng:testng:6.4",
"text": "org.apache.commons:commons-text:1.4",
"zip4j": "net.lingala.zip4j:zip4j:1.3.2"
"zip4j": "net.lingala.zip4j:zip4j:1.3.2",
"freemaker": "org.freemarker:freemarker:2.3.14",
]
]

Expand Down
1 change: 1 addition & 0 deletions tony-core/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ dependencies {
compile deps.external.jackson_databind
compile deps.external.text
compile deps.external.zip4j
compile deps.external.freemaker
compile(deps.hadoop.common) {
exclude group: 'org.slf4j'
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -485,6 +485,7 @@ private boolean prepare() throws IOException {
.runtimeType(frameworkType)
.session(session)
.amLogUrl(amLogUrl)
.appId(appIdString)
.build();
String dashboardHttpUrl = dashboardHttpServer.start();
this.dashboardHttpServer = dashboardHttpServer;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,30 +15,47 @@
*/
package com.linkedin.tony.dashboard;

import com.google.common.annotations.VisibleForTesting;
import com.linkedin.tony.TonySession;
import com.linkedin.tony.rpc.TaskInfo;
import com.linkedin.tony.util.Utils;
import com.sun.net.httpserver.HttpServer;
import freemarker.template.Configuration;
import freemarker.template.DefaultObjectWrapper;
import freemarker.template.ObjectWrapper;
import freemarker.template.Template;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import javax.annotation.Nonnull;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.StringWriter;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class DashboardHttpServer implements AutoCloseable {
private static final Log LOG = LogFactory.getLog(DashboardHttpServer.class);

private static final String TEMPLATE_HTML_FILE = "template.html";

private TonySession tonySession;
private String amHostName;
private String amLogUrl;
private String runtimeType;
private String appId;

private String tensorboardUrl;

Expand All @@ -48,44 +65,94 @@ public class DashboardHttpServer implements AutoCloseable {

private boolean started = false;

private Template template;

private DashboardHttpServer(
@Nonnull TonySession tonySession, @Nonnull String amLogUrl,
@Nonnull String runtimeType, @Nonnull String amHostName) {
@Nonnull String runtimeType, @Nonnull String amHostName,
@Nonnull String appId) {
assert tonySession != null;
this.tonySession = tonySession;
this.amLogUrl = amLogUrl;
this.runtimeType = runtimeType;
this.amHostName = amHostName;
this.appId = appId;
}

public String start() throws Exception {
final int port = getAvailablePort();
this.serverPort = port;
LOG.info("Starting dashboard server, http url: " + amHostName + ":" + port);

this.executorService = Executors.newSingleThreadExecutor();
executorService.submit(() -> {
try {
final HttpServer server = HttpServer.create(new InetSocketAddress(port), 0);

server.createContext("/", httpExchange -> {
byte[] response = getDashboardContent().getBytes("UTF-8");

httpExchange.getResponseHeaders().add("Content-Type", "text/html; charset=UTF-8");
httpExchange.sendResponseHeaders(200, response.length);

OutputStream out = httpExchange.getResponseBody();
out.write(response);
out.close();
});
server.start();
this.started = true;
} catch (Throwable tr) {
LOG.error("Errors on starting web dashboard server.", tr);
}
});
Configuration configuration = new Configuration();

Path tempDir = Files.createTempDirectory("template");
tempDir.toFile().deleteOnExit();
Path templateFilePath = Paths.get(tempDir.toAbsolutePath().toString(), "template.html");
try (InputStream stream = this.getClass().getClassLoader()
.getResourceAsStream("dashboard/template.html")) {
Files.copy(stream, templateFilePath);
}
configuration.setDirectoryForTemplateLoading(
tempDir.toFile()
);
configuration.setObjectWrapper(new DefaultObjectWrapper());
this.template = configuration.getTemplate(TEMPLATE_HTML_FILE);

byte[] cssResource = IOUtils.toByteArray(
this.getClass().getClassLoader()
.getResourceAsStream("dashboard/static/css/bootstrap.min.css")
);
byte[] logoResource = IOUtils.toByteArray(
this.getClass().getClassLoader()
.getResourceAsStream("dashboard/static/img/TonY-icon-color.png")
);

try {
final HttpServer server = HttpServer.create(new InetSocketAddress(port), 0);

server.createContext("/", httpExchange -> {
byte[] response = getDashboardContent().getBytes("UTF-8");

httpExchange.getResponseHeaders().add("Content-Type", "text/html; charset=UTF-8");
httpExchange.sendResponseHeaders(200, response.length);

OutputStream out = httpExchange.getResponseBody();
out.write(response);
out.close();
});

server.createContext("/static/img/TonY-icon-color.png", httpExchange -> {
httpExchange.getResponseHeaders().add("Content-Type", "image/png");
httpExchange.sendResponseHeaders(200, logoResource.length);
OutputStream out = httpExchange.getResponseBody();
out.write(logoResource);
out.close();
});

server.createContext("/static/css/bootstrap.min.css", httpExchange -> {
httpExchange.getResponseHeaders().add("Content-Type", "text/css");
httpExchange.sendResponseHeaders(200, cssResource.length);
OutputStream out = httpExchange.getResponseBody();
out.write(cssResource);
out.close();
});

server.setExecutor(executorService);
server.start();
this.started = true;
} catch (Throwable tr) {
LOG.error("Errors on starting web dashboard server.", tr);
}

return amHostName + ":" + serverPort;
}

@VisibleForTesting
protected int getServerPort() {
return serverPort;
}

private int getAvailablePort() throws IOException {
try (ServerSocket serverSocket = new ServerSocket(0)) {
return serverSocket.getLocalPort();
Expand All @@ -101,69 +168,59 @@ public void registerTensorboardUrl(String tbUrl) {
}

private String getDashboardContent() {
/**
* TODO: Need to introduce the template engine to support pretty html page.
*/
StringBuilder builder = new StringBuilder();
builder.append("<!DOCTYPE html><html>");
builder.append("<head>");
builder.append("<title>TonY Dashboard</title>");
builder.append("<style> table, th, td { border: 1px solid black; border-collapse: collapse; } th, td {"
+ " padding: 8px; } </style>");
builder.append("</head>");
builder.append("<body>");

builder.append("<h2>TonY Dashboard</h2>");
builder.append("<hr/>");
builder.append("<p>ApplicationMaster log url: <a href=\"" + amLogUrl + "\">" + amLogUrl + "</a></p>");
if (tensorboardUrl != null) {
builder.append("<p>Tensorboard log url: <a href=\"" + tensorboardUrl + "\">"
+ tensorboardUrl + "</a></p>");
} else {
builder.append("<p>Tensorboard log url: (not started yet)</p>");
}
builder.append("<p>Runtime type: " + runtimeType + "</p>");

if (tonySession != null) {
int total = tonySession.getTotalTasks();
int registered = tonySession.getNumRegisteredTasks();
builder.append("<p>Total task executors: "
+ total
+ ", registered task executors: "
+ registered
+ "</p>"
);
}

builder.append("<hr/>");
builder.append("<h4>Task Executors</h4>");

if (tonySession != null) {
builder.append("<table style=\"width:100%\">"
+ " <tr>"
+ " <th>task executor</th>"
+ " <th>state</th>"
+ " <th>log url</th>"
+ " </tr>");
tonySession.getTonyTasks().values().stream()
.flatMap(x -> Arrays.stream(x))
.filter(task -> task != null)
.filter(task -> task.getTaskInfo() != null)
.forEach(task -> {
TaskInfo taskInfo = task.getTaskInfo();
builder.append("<tr>"
+ " <td>" + taskInfo.getName() + ":" + taskInfo.getIndex() + "</td>"
+ " <td>" + taskInfo.getStatus() + "</td>"
+ " <td><a href=\"" + taskInfo.getUrl() + "\">" + taskInfo.getUrl() + "</a></td>"
+ " </tr>");
});
builder.append("</table>");
}
try {
Map<String, Object> dataMap = new HashMap<>();
dataMap.put("appMasterLogUrl", amLogUrl);
dataMap.put("tensorboardLogUrl", tensorboardUrl == null ? "Not started yet." : tensorboardUrl);
dataMap.put("runtimeType", runtimeType);
dataMap.put("appId", StringUtils.defaultString(appId, ""));
dataMap.put("amHostPort", String.format("http://%s:%s", amHostName, serverPort));

int total = 0;
int registered = 0;
if (tonySession != null) {
total = tonySession.getTotalTasks();
registered = tonySession.getNumRegisteredTasks();
}
dataMap.put("registeredNumber", registered);
dataMap.put("taskNumber", total);

StringBuilder tableContentBuilder = new StringBuilder();
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
if (tonySession != null) {
tonySession.getTonyTasks().values().stream()
.flatMap(x -> Arrays.stream(x))
.filter(task -> task != null)
.filter(task -> task.getTaskInfo() != null)
.forEach(task -> {
TaskInfo taskInfo = task.getTaskInfo();

tableContentBuilder.append(String.format(
"<tr> "
+ "<th scope=\"row\">%s</th> "
+ "<td>%s</td> "
+ "<td>%s</td> "
+ "<td>%s</td> "
+ "<td><a href=\"%s}\">LINK</td> "
+ "</tr>",
taskInfo.getName() + ":" + taskInfo.getIndex(),
taskInfo.getStatus().name(),
task.getStartTime() == 0 ? "" : format.format(task.getStartTime()),
task.getEndTime() == 0 ? "" : format.format(task.getEndTime()),
taskInfo.getUrl()
));
});
}

builder.append("</body>");
builder.append("</html>");
dataMap.put("tableContent", tableContentBuilder.toString());

return builder.toString();
StringWriter writer = new StringWriter();
template.process(dataMap, writer, ObjectWrapper.BEANS_WRAPPER);
return writer.toString();
} catch (Exception e) {
LOG.error("Errors on returning html content.", e);
}
return "error";
}

/**
Expand All @@ -190,6 +247,7 @@ public static class DashboardHttpServerBuilder {
private String amLogUrl;
private String runtimeType;
private String amHostName;
private String appId;

private DashboardHttpServerBuilder() {
// ignore
Expand All @@ -215,8 +273,13 @@ public DashboardHttpServerBuilder amHostName(String amHostName) {
return this;
}

public DashboardHttpServerBuilder appId(String appId) {
this.appId = appId;
return this;
}

public DashboardHttpServer build() {
return new DashboardHttpServer(tonySession, amLogUrl, runtimeType, amHostName);
return new DashboardHttpServer(tonySession, amLogUrl, runtimeType, amHostName, appId);
}
}
}
Loading

0 comments on commit 7e2e8a5

Please sign in to comment.