Skip to content

Commit

Permalink
#4438 - Buffer server side timing headers
Browse files Browse the repository at this point in the history
- Buffer the headers and commit them only at the end of the request
- Avoid generating a timer header in CasDoctor if no checks are enabled
  • Loading branch information
reckart committed Jan 21, 2024
1 parent 2a32008 commit ef692da
Show file tree
Hide file tree
Showing 3 changed files with 138 additions and 23 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,10 @@ public boolean analyze(Project aProject, CAS aCas, List<LogMessage> aMessages,
boolean aFatalChecks)
throws CasDoctorException
{
if (activeChecks.isEmpty()) {
return true;
}

long tStart = currentTimeMillis();

boolean ok = true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,10 @@
*/
package de.tudarmstadt.ukp.inception.support.wicket;

import static org.apache.wicket.RuntimeConfigurationType.DEVELOPMENT;

import java.lang.invoke.MethodHandles;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.Properties;

import org.apache.wicket.Application;
import org.apache.wicket.Component;
Expand All @@ -30,15 +29,17 @@
import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.markup.head.IHeaderResponse;
import org.apache.wicket.markup.head.OnDomReadyHeaderItem;
import org.apache.wicket.request.IRequestHandler;
import org.apache.wicket.request.Response;
import org.apache.wicket.request.cycle.IRequestCycleListener;
import org.apache.wicket.request.cycle.PageRequestHandlerTracker;
import org.apache.wicket.request.cycle.RequestCycle;
import org.apache.wicket.request.http.WebResponse;
import org.apache.wicket.response.filter.IResponseFilter;
import org.apache.wicket.util.string.AppendingStringBuffer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import de.tudarmstadt.ukp.inception.support.SettingsUtil;

public final class WicketUtil
{
private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
Expand Down Expand Up @@ -101,32 +102,46 @@ public static void serverTiming(String aKey, String aDescription, long aTime)
return;
}

Properties settings = SettingsUtil.getSettings();
if (!DEVELOPMENT.equals(app.getConfigurationType())
&& !"true".equalsIgnoreCase(settings.getProperty("debug.sendServerSideTimings"))) {
var requestCycle = RequestCycle.get();
if (requestCycle == null) {
return;
}

RequestCycle requestCycle = RequestCycle.get();
if (requestCycle == null) {
return;
var thl = getTimingListener(requestCycle);
if (thl != null) {
thl.add(aKey, aDescription, aTime);
}
}

Response response = requestCycle.getResponse();
if (response instanceof WebResponse) {
WebResponse webResponse = (WebResponse) response;
StringBuilder sb = new StringBuilder();
sb.append(aKey);
if (aDescription != null) {
sb.append(";desc=\"");
sb.append(aDescription);
sb.append("\"");
public static void installTimingListener(RequestCycle requestCycle)
{
TimingHeaderListener thl = null;
var i = requestCycle.getListeners().iterator();
while (i.hasNext()) {
var listener = i.next();
if (listener instanceof TimingHeaderListener foundThl) {
thl = foundThl;
}
sb.append(";dur=");
sb.append(aTime);
}

if (thl == null) {
thl = new TimingHeaderListener();
requestCycle.getListeners().add(thl);
}
}

webResponse.addHeader("Server-Timing", sb.toString());
private static TimingHeaderListener getTimingListener(RequestCycle requestCycle)
{
TimingHeaderListener thl = null;
var i = requestCycle.getListeners().iterator();
while (i.hasNext()) {
var listener = i.next();
if (listener instanceof TimingHeaderListener foundThl) {
thl = foundThl;
}
}

return thl;
}

public static void refreshPage(AjaxRequestTarget aTarget, Page aPage)
Expand All @@ -147,4 +162,85 @@ public static String wrapInTryCatch(CharSequence aJsCall)
{
return " tryCatch(() => {" + aJsCall + "}); ";
}

private record TimingRecord(String key, String description, long time) {}

public static final class TimingHeaderListener
implements IRequestCycleListener
{
private List<TimingRecord> records = new ArrayList<>();

void add(String aKey, String aDescription, long aTime)
{
records.add(new TimingRecord(aKey, aDescription, aTime));
}

@Override
public void onRequestHandlerExecuted(RequestCycle aCycle, IRequestHandler aHandler)
{
renderTimingHeaders(aCycle);
}

private void renderTimingHeaders(RequestCycle aCycle)
{
Response response = aCycle.getResponse();
if (response instanceof WebResponse) {
var webResponse = (WebResponse) response;

for (var rec : records) {
var sb = new StringBuilder();
sb.append(rec.key);
if (rec.description != null) {
sb.append(";desc=\"");
sb.append(rec.description);
sb.append("\"");
}
sb.append(";dur=");
sb.append(rec.time);

webResponse.addHeader("Server-Timing", sb.toString());
}
}
records.clear();
}
}

public static final class TimingResponseFilter
implements IResponseFilter
{

@Override
public AppendingStringBuffer filter(AppendingStringBuffer aResponseBuffer)
{
var requestCycle = RequestCycle.get();
if (requestCycle != null) {
var thl = getTimingListener(requestCycle);
if (thl != null) {
thl.renderTimingHeaders(requestCycle);

}
}

return aResponseBuffer;
}
}

public static void installTimingListeners(Application aApplication)
{
// Register a timing listener early in the request cycle so we can buffer timing information
// inside that listener
aApplication.getRequestCycleListeners().add(new IRequestCycleListener()
{
@Override
public void onBeginRequest(RequestCycle aCycle)
{
WicketUtil.installTimingListener(aCycle);
}
});

// Register a timing listener late in the rendering process such that we can render the
// timing headers latest then
aApplication.getRequestCycleSettings()
.addResponseFilter(new WicketUtil.TimingResponseFilter());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@
import de.tudarmstadt.ukp.inception.support.kendo.KendoResourceBehavior;
import de.tudarmstadt.ukp.inception.support.kendo.WicketJQueryFocusPatchBehavior;
import de.tudarmstadt.ukp.inception.support.wicket.PatternMatchingCrossOriginEmbedderPolicyRequestCycleListener;
import de.tudarmstadt.ukp.inception.support.wicket.WicketUtil;
import de.tudarmstadt.ukp.inception.support.wicket.resource.ContextSensitivePackageStringResourceLoader;
import de.tudarmstadt.ukp.inception.ui.core.ErrorListener;
import de.tudarmstadt.ukp.inception.ui.core.ErrorTestPage;
Expand Down Expand Up @@ -142,6 +143,8 @@ protected void init()

installSpringSecurityContextPropagationRequestCycleListener();

installTimingListener();

// Enforce COEP while inheriting any exemptions that might already have been set e.g. via
// WicketApplicationInitConfiguration beans
getSecuritySettings().setCrossOriginEmbedderPolicyConfiguration(ENFORCING,
Expand All @@ -154,6 +157,18 @@ protected void init()
initOnce();
}

private void installTimingListener()
{
var settings = SettingsUtil.getSettings();
if (!DEVELOPMENT.equals(getConfigurationType())
&& !"true".equalsIgnoreCase(settings.getProperty("debug.sendServerSideTimings"))) {
return;
}

WicketUtil.installTimingListeners(this);

}

@Override
protected void validateInit()
{
Expand Down

0 comments on commit ef692da

Please sign in to comment.