-
Notifications
You must be signed in to change notification settings - Fork 188
produce userdata for VSCode to consume #40
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
Changes from all commits
c2ffccd
f235179
c4bbc5e
db145d5
6777dc9
d1ff793
129f598
62de460
9718c20
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,134 @@ | ||
| /******************************************************************************* | ||
| * Copyright (c) 2017 Microsoft Corporation and others. | ||
| * All rights reserved. This program and the accompanying materials | ||
| * are made available under the terms of the Eclipse Public License v1.0 | ||
| * which accompanies this distribution, and is available at | ||
| * http://www.eclipse.org/legal/epl-v10.html | ||
| * | ||
| * Contributors: | ||
| * Microsoft Corporation - initial API and implementation | ||
| *******************************************************************************/ | ||
|
|
||
| package com.microsoft.java.debug.core; | ||
|
|
||
| import java.util.HashMap; | ||
| import java.util.Map; | ||
| import java.util.logging.Level; | ||
| import java.util.logging.Logger; | ||
|
|
||
| import com.google.gson.JsonElement; | ||
| import com.microsoft.java.debug.core.adapter.AdapterUtils; | ||
| import com.microsoft.java.debug.core.adapter.JsonUtils; | ||
| import com.microsoft.java.debug.core.adapter.Messages.Request; | ||
| import com.microsoft.java.debug.core.adapter.Messages.Response; | ||
|
|
||
| public class UsageDataSession { | ||
| private static final Logger usageDataLogger = Logger.getLogger(Configuration.USAGE_DATA_LOGGER_NAME); | ||
| private static final long RESPONSE_MAX_DELAY_MS = 1000; | ||
| private static final ThreadLocal<String> sessionGuid = new InheritableThreadLocal<>(); | ||
|
|
||
| private long startAt = -1; | ||
| private long stopAt = -1; | ||
| private Map<String, Integer> commandCountMap = new HashMap<>(); | ||
| private Map<String, Integer> breakpointCountMap = new HashMap<>(); | ||
| private Map<Integer, RequestEvent> requestEventMap = new HashMap<>(); | ||
|
|
||
| public static void setSessionGuid(String guid) { | ||
| sessionGuid.set(guid); | ||
| } | ||
|
|
||
| public static String getSessionGuid() { | ||
| return sessionGuid.get(); | ||
| } | ||
|
|
||
| class RequestEvent { | ||
| Request request; | ||
| long timestamp; | ||
|
|
||
| RequestEvent(Request request, long timestamp) { | ||
| this.request = request; | ||
| this.timestamp = timestamp; | ||
| } | ||
| } | ||
|
|
||
| public void reportStart() { | ||
| startAt = System.currentTimeMillis(); | ||
| } | ||
|
|
||
| public void reportStop() { | ||
| stopAt = System.currentTimeMillis(); | ||
| } | ||
|
|
||
| /** | ||
| * Record usage data from request. | ||
| */ | ||
| public void recordRequest(Request request) { | ||
| try { | ||
| requestEventMap.put(request.seq, new RequestEvent(request, System.currentTimeMillis())); | ||
|
|
||
| // cmd count | ||
| commandCountMap.put(request.command, commandCountMap.getOrDefault(request.command, 0) + 1); | ||
|
|
||
| // bp count | ||
| if ("setBreakpoints".equals(request.command)) { | ||
| String fileIdentifier = "unknown file"; | ||
| JsonElement pathElement = request.arguments.get("source").getAsJsonObject().get("path"); | ||
| JsonElement nameElement = request.arguments.get("source").getAsJsonObject().get("name"); | ||
| if (pathElement != null) { | ||
| fileIdentifier = pathElement.getAsString(); | ||
| } else if (nameElement != null) { | ||
| fileIdentifier = nameElement.getAsString(); | ||
| } | ||
| String filenameHash = AdapterUtils.getSHA256HexDigest(fileIdentifier); | ||
| int bpCount = request.arguments.get("breakpoints").getAsJsonArray().size(); | ||
| breakpointCountMap.put(filenameHash, breakpointCountMap.getOrDefault(filenameHash, 0) + bpCount); | ||
| } | ||
| } catch (Throwable e) { | ||
| // ignore it | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Record usage data from response. | ||
| */ | ||
| public void recordResponse(Response response) { | ||
| try { | ||
| long responseMillis = System.currentTimeMillis(); | ||
| long requestMillis = responseMillis; | ||
| String command = null; | ||
|
|
||
| RequestEvent requestEvent = requestEventMap.getOrDefault(response.request_seq, null); | ||
| if (requestEvent != null) { | ||
| command = requestEvent.request.command; | ||
| requestMillis = requestEvent.timestamp; | ||
| requestEventMap.remove(response.request_seq); | ||
| } | ||
| long duration = responseMillis - requestMillis; | ||
|
|
||
| if (!response.success || duration > RESPONSE_MAX_DELAY_MS) { | ||
| Map<String, Object> props = new HashMap<>(); | ||
| props.put("duration", duration); | ||
| props.put("command", command); | ||
| props.put("success", response.success); | ||
| // directly report abnormal response. | ||
| usageDataLogger.log(Level.WARNING, "abnormal response", props); | ||
| } | ||
| } catch (Throwable e) { | ||
| // ignore it | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Submit summary of usage data in current session. | ||
| */ | ||
| public void submitUsageData() { | ||
| Map<String, String> props = new HashMap<>(); | ||
|
|
||
| props.put("sessionStartAt", String.valueOf(startAt)); | ||
| props.put("sessionStopAt", String.valueOf(stopAt)); | ||
| props.put("commandCount", JsonUtils.toJson(commandCountMap)); | ||
| props.put("breakpointCount", JsonUtils.toJson(breakpointCountMap)); | ||
| usageDataLogger.log(Level.INFO, "session usage data summary", props); | ||
| } | ||
|
|
||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,96 @@ | ||
| /******************************************************************************* | ||
| * Copyright (c) 2017 Microsoft Corporation and others. | ||
| * All rights reserved. This program and the accompanying materials | ||
| * are made available under the terms of the Eclipse Public License v1.0 | ||
| * which accompanies this distribution, and is available at | ||
| * http://www.eclipse.org/legal/epl-v10.html | ||
| * | ||
| * Contributors: | ||
| * Microsoft Corporation - initial API and implementation | ||
| *******************************************************************************/ | ||
|
|
||
| package com.microsoft.java.debug.core; | ||
|
|
||
| import java.io.PrintWriter; | ||
| import java.io.StringWriter; | ||
| import java.util.HashMap; | ||
| import java.util.Map; | ||
| import java.util.concurrent.ConcurrentLinkedQueue; | ||
|
|
||
| public class UsageDataStore { | ||
| private ConcurrentLinkedQueue<Object> queue; | ||
| private static final int QUEUE_MAX_SIZE = 10000; | ||
|
|
||
| /** | ||
| * Constructor. | ||
| */ | ||
| private UsageDataStore() { | ||
| queue = new ConcurrentLinkedQueue<>(); | ||
| } | ||
|
|
||
| private static final class SingletonHolder { | ||
| private static final UsageDataStore INSTANCE = new UsageDataStore(); | ||
| } | ||
|
|
||
| /** | ||
| * Fetch all pending user data records | ||
| * @return List of user data Object. | ||
| */ | ||
| public synchronized Object[] fetchAll() { | ||
| Object[] ret = queue.toArray(); | ||
| queue.clear(); | ||
| return ret; | ||
| } | ||
|
|
||
| public static UsageDataStore getInstance() { | ||
| return SingletonHolder.INSTANCE; | ||
| } | ||
|
|
||
| /** | ||
| * Log user data Object into the queue. | ||
| */ | ||
| public void logSessionData(String desc, Map<String, String> props) { | ||
| if (queue == null) { | ||
| return; | ||
| } | ||
| Map<String, String> sessionEntry = new HashMap<>(); | ||
| sessionEntry.put("scope", "session"); | ||
| sessionEntry.put("debugSessionId", UsageDataSession.getSessionGuid()); | ||
| if (desc != null) { | ||
| sessionEntry.put("description", desc); | ||
| } | ||
| if (props != null) { | ||
| sessionEntry.putAll(props); | ||
| } | ||
| enqueue(sessionEntry); | ||
| } | ||
|
|
||
| /** | ||
| * Log Exception details into queue. | ||
| */ | ||
| public void logErrorData(String desc, Throwable th) { | ||
| if (queue == null) { | ||
| return; | ||
| } | ||
| Map<String, String> errorEntry = new HashMap<>(); | ||
| errorEntry.put("scope", "exception"); | ||
| errorEntry.put("deubgSessionId", UsageDataSession.getSessionGuid()); | ||
| if (desc != null) { | ||
| errorEntry.put("description", desc); | ||
| } | ||
| if (th != null) { | ||
| errorEntry.put("message", th.getMessage()); | ||
| StringWriter sw = new StringWriter(); | ||
| th.printStackTrace(new PrintWriter(sw)); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. better to close StringWriter PrintWriter |
||
| errorEntry.put("stackTrace", sw.toString()); | ||
| } | ||
| enqueue(errorEntry); | ||
| } | ||
|
|
||
| private synchronized void enqueue(Object object) { | ||
| if (queue.size() > QUEUE_MAX_SIZE) { | ||
| queue.poll(); | ||
| } | ||
| queue.add(object); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -31,10 +31,10 @@ | |
| import java.util.regex.Pattern; | ||
|
|
||
| import com.microsoft.java.debug.core.Configuration; | ||
| import com.microsoft.java.debug.core.UsageDataSession; | ||
|
|
||
| public class ProtocolServer { | ||
| private static final Logger logger = Logger.getLogger(Configuration.LOGGER_NAME); | ||
|
|
||
| private static final int BUFFER_SIZE = 4096; | ||
| private static final String TWO_CRLF = "\r\n\r\n"; | ||
| private static final Pattern CONTENT_LENGTH_MATCHER = Pattern.compile("Content-Length: (\\d+)"); | ||
|
|
@@ -53,6 +53,8 @@ public class ProtocolServer { | |
|
|
||
| private IDebugAdapter debugAdapter; | ||
|
|
||
| private UsageDataSession usageDataSession = new UsageDataSession(); | ||
|
|
||
| /** | ||
| * Constructs a protocol server instance based on the given input stream and output stream. | ||
| * @param input | ||
|
|
@@ -84,6 +86,7 @@ public ProtocolServer(InputStream input, OutputStream output, IProviderContext c | |
| * A while-loop to parse input data and send output data constantly. | ||
| */ | ||
| public void start() { | ||
| usageDataSession.reportStart(); | ||
| char[] buffer = new char[BUFFER_SIZE]; | ||
| try { | ||
| while (!this.terminateSession) { | ||
|
|
@@ -104,7 +107,9 @@ public void start() { | |
| * Sets terminateSession flag to true. And the dispatcher loop will be terminated after current dispatching operation finishes. | ||
| */ | ||
| public void stop() { | ||
| usageDataSession.reportStop(); | ||
| this.terminateSession = true; | ||
| usageDataSession.submitUsageData(); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i'm not sure, but could it possibly be wrapped in sth like a finally block? namely should we report when exception thrown during the session? |
||
| } | ||
|
|
||
| /** | ||
|
|
@@ -193,6 +198,7 @@ private void dispatchRequest(String request) { | |
| try { | ||
| logger.info("\n[REQUEST]\n" + request); | ||
| Messages.Request message = JsonUtils.fromJson(request, Messages.Request.class); | ||
| usageDataSession.recordRequest(message); | ||
| if (message.type.equals("request")) { | ||
| synchronized (this) { | ||
| this.isDispatchingData = true; | ||
|
|
@@ -204,6 +210,7 @@ private void dispatchRequest(String request) { | |
| this.stop(); | ||
| } | ||
| sendMessage(response); | ||
| usageDataSession.recordResponse(response); | ||
| } catch (Exception e) { | ||
| logger.log(Level.SEVERE, String.format("Dispatch debug protocol error: %s", e.toString()), e); | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we have multiple sessions going on at the same time, they will share the same session id, which is not expected.