Reduce the size (and cost) of the host page

We strip any unnecessary whitespace from the header/footer and the
host page HTML, compacting it to a smaller size even before we pass
it off for gzip compression.  This reduces the size of final the
document we have to compress and send to the browser.

For anonymous users we now use a compressed anonymous copy of the
host page, so we only have to dump the byte array to the network
socket.  This slightly improves response time for the initial page
returned to the browser.

For identified users we put the host page together from two different
byte arrays, inserting into the middle the user's account data.
This is then compressed if the browser wants gzip compression.

Change-Id: I82665d9938a998ccc8b5cb2f792c8798974dcf85
Signed-off-by: Shawn O. Pearce <sop@google.com>
This commit is contained in:
Shawn O. Pearce
2009-12-29 13:42:38 -08:00
parent 67288bf63e
commit 4169a42fe9
5 changed files with 94 additions and 49 deletions

View File

@@ -18,6 +18,6 @@ import com.google.gerrit.reviewdb.Account;
/** Data sent as part of the host page, to bootstrap the UI. */
public class HostPageData {
public Account userAccount;
public Account account;
public GerritConfig config;
}

View File

@@ -223,8 +223,8 @@ public class Gerrit implements EntryPoint {
hpd.load(new GerritCallback<HostPageData>() {
public void onSuccess(final HostPageData result) {
myConfig = result.config;
if (result.userAccount != null) {
myAccount = result.userAccount;
if (result.account != null) {
myAccount = result.account;
applyUserPreferences();
}
onModuleLoad2(gStarting);

View File

@@ -23,6 +23,6 @@ import com.google.gwtjsonrpc.client.RpcImpl.Version;
@RpcImpl(version = Version.V2_0)
interface HostPageDataService extends RemoteJsonService {
@HostPageCache(name = "gerrit_hostpagedata_obj", once = true)
@HostPageCache(name = "gerrit_hostpagedata", once = true)
void load(AsyncCallback<HostPageData> callback);
}

View File

@@ -41,6 +41,10 @@ import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
/** Utility functions to deal with HTML using W3C DOM operations. */
public class HtmlDomUtil {
@@ -146,7 +150,9 @@ public class HtmlDomUtil {
try {
try {
try {
return newBuilder().parse(in);
final Document doc = newBuilder().parse(in);
compact(doc);
return doc;
} catch (SAXException e) {
throw new IOException("Error reading " + name, e);
} catch (ParserConfigurationException e) {
@@ -160,6 +166,21 @@ public class HtmlDomUtil {
}
}
private static void compact(final Document doc) {
try {
final String expr = "//text()[normalize-space(.) = '']";
final XPathFactory xp = XPathFactory.newInstance();
final XPathExpression e = xp.newXPath().compile(expr);
NodeList empty = (NodeList) e.evaluate(doc, XPathConstants.NODESET);
for (int i = 0; i < empty.getLength(); i++) {
Node node = empty.item(i);
node.getParentNode().removeChild(node);
}
} catch (XPathExpressionException e) {
// Don't do the whitespace removal.
}
}
/** Read a Read a UTF-8 text file from our CLASSPATH and return it. */
public static String readFile(final Class<?> context, final String name)
throws IOException {
@@ -175,17 +196,14 @@ public class HtmlDomUtil {
}
/** Parse an XHTML file from the local drive and return the instance. */
public static Document parseFile(final File parentDir, final String name)
throws IOException {
if (parentDir == null) {
return null;
}
final File path = new File(parentDir, name);
public static Document parseFile(final File path) throws IOException {
try {
final InputStream in = new FileInputStream(path);
try {
try {
return newBuilder().parse(in);
final Document doc = newBuilder().parse(in);
compact(doc);
return doc;
} catch (SAXException e) {
throw new IOException("Error reading " + path, e);
} catch (ParserConfigurationException e) {
@@ -239,6 +257,7 @@ public class HtmlDomUtil {
factory.setValidating(false);
factory.setExpandEntityReferences(false);
factory.setIgnoringComments(true);
factory.setCoalescing(true);
final DocumentBuilder parser = factory.newDocumentBuilder();
return parser;
}

View File

@@ -53,6 +53,7 @@ public class HostPageServlet extends HttpServlet {
private static final Logger log =
LoggerFactory.getLogger(HostPageServlet.class);
private static final boolean IS_DEV = Boolean.getBoolean("Gerrit.GwtDevMode");
private static final String HPD_ID = "gerrit_hostpagedata";
private final Provider<CurrentUser> currentUser;
private final GerritConfig config;
@@ -120,31 +121,8 @@ public class HostPageServlet extends HttpServlet {
asScript(scriptNode);
}
private void injectJson(final Document hostDoc, final String id,
final Object obj) {
final Element scriptNode = HtmlDomUtil.find(hostDoc, id);
if (scriptNode == null) {
return;
}
while (scriptNode.getFirstChild() != null) {
scriptNode.removeChild(scriptNode.getFirstChild());
}
if (obj == null) {
scriptNode.getParentNode().removeChild(scriptNode);
return;
}
final StringWriter w = new StringWriter();
w.write("<!--\n");
w.write("var ");
w.write(id);
w.write("_obj=");
JsonServlet.defaultGsonBuilder().create().toJson(obj, w);
w.write(";\n// -->\n");
asScript(scriptNode);
scriptNode.appendChild(hostDoc.createCDATASection(w.toString()));
private void json(final Object data, final StringWriter w) {
JsonServlet.defaultGsonBuilder().create().toJson(data, w);
}
private static void asScript(final Element scriptNode) {
@@ -172,22 +150,26 @@ public class HostPageServlet extends HttpServlet {
@Override
protected void doGet(final HttpServletRequest req,
final HttpServletResponse rsp) throws IOException {
final HostPageData pageData = new HostPageData();
pageData.config = config;
final Page page = get();
final byte[] raw;
final CurrentUser user = currentUser.get();
if (user instanceof IdentifiedUser) {
pageData.userAccount = ((IdentifiedUser) user).getAccount();
final StringWriter w = new StringWriter();
w.write(HPD_ID + ".account=");
json(((IdentifiedUser) user).getAccount(), w);
w.write(";");
final byte[] userData = w.toString().getBytes("UTF-8");
raw = concat(page.part1, userData, page.part2);
} else {
raw = page.full;
}
final Document peruser = HtmlDomUtil.clone(get().hostDoc);
injectJson(peruser, "gerrit_hostpagedata", pageData);
final byte[] raw = HtmlDomUtil.toUTF8(peruser);
final byte[] tosend;
if (RPCServletUtils.acceptsGzipEncoding(req)) {
rsp.setHeader("Content-Encoding", "gzip");
tosend = HtmlDomUtil.compress(raw);
tosend = raw == page.full ? page.full_gz : HtmlDomUtil.compress(raw);
} else {
tosend = raw;
}
@@ -206,6 +188,20 @@ public class HostPageServlet extends HttpServlet {
}
}
private static byte[] concat(byte[] p1, byte[] p2, byte[] p3) {
final byte[] r = new byte[p1.length + p2.length + p3.length];
int p = 0;
p = append(p1, r, p);
p = append(p2, r, p);
p = append(p3, r, p);
return r;
}
private static int append(byte[] src, final byte[] dst, int p) {
System.arraycopy(src, 0, dst, p, src.length);
return p + src.length;
}
private static class FileInfo {
private final File path;
private final long time;
@@ -221,17 +217,47 @@ public class HostPageServlet extends HttpServlet {
}
private class Page {
final Document hostDoc;
private final FileInfo css;
private final FileInfo header;
private final FileInfo footer;
final byte[] part1;
final byte[] part2;
final byte[] full;
final byte[] full_gz;
Page() throws IOException {
hostDoc = HtmlDomUtil.clone(template);
Document hostDoc = HtmlDomUtil.clone(template);
css = injectCssFile(hostDoc, "gerrit_sitecss", site.site_css);
header = injectXmlFile(hostDoc, "gerrit_header", site.site_header);
footer = injectXmlFile(hostDoc, "gerrit_footer", site.site_footer);
final HostPageData pageData = new HostPageData();
pageData.config = config;
final StringWriter w = new StringWriter();
w.write("var " + HPD_ID + "=");
json(pageData, w);
w.write(";");
final Element data = HtmlDomUtil.find(hostDoc, HPD_ID);
if (data == null) {
throw new IOException("No " + HPD_ID + " in host page HTML");
}
asScript(data);
data.appendChild(hostDoc.createTextNode(w.toString()));
data.appendChild(hostDoc.createComment(HPD_ID));
final String raw = HtmlDomUtil.toString(hostDoc);
final int p = raw.indexOf("<!--" + HPD_ID);
if (p < 0) {
throw new IOException("No tag in transformed host page HTML");
}
part1 = raw.substring(0, p).getBytes("UTF-8");
part2 = raw.substring(raw.indexOf('>', p) + 1).getBytes("UTF-8");
full = concat(part1, part2, new byte[0]);
full_gz = HtmlDomUtil.compress(full);
}
boolean isStale() {
@@ -273,7 +299,7 @@ public class HostPageServlet extends HttpServlet {
banner.removeChild(banner.getFirstChild());
}
Document html = HtmlDomUtil.parseFile(src.getParentFile(), src.getName());
Document html = HtmlDomUtil.parseFile(src);
if (html == null) {
banner.getParentNode().removeChild(banner);
return info;