Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Optimize the TonY AM web dashboard page #667

Merged
merged 1 commit into from
May 22, 2022
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
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