changeset 1578:43864ae00dbf

Indicate profiling status in UI Reviewed-by: jerboaa Review-thread: http://icedtea.classpath.org/pipermail/thermostat/2014-November/011734.html
author Omair Majid <omajid@redhat.com>
date Fri, 28 Nov 2014 16:37:02 -0500
parents bd5e7855f9f3
children 783d3874f9ff
files vm-profiler/agent/src/main/java/com/redhat/thermostat/vm/profiler/agent/internal/ProfileVmRequestReceiver.java vm-profiler/agent/src/test/java/com/redhat/thermostat/vm/profiler/agent/internal/ProfileVmRequestReceiverTest.java vm-profiler/client-cli/src/main/java/com/redhat/thermostat/vm/profiler/client/cli/internal/LocaleResources.java vm-profiler/client-cli/src/main/java/com/redhat/thermostat/vm/profiler/client/cli/internal/ProfileVmCommand.java vm-profiler/client-cli/src/main/resources/com/redhat/thermostat/vm/profiler/client/cli/internal/strings.properties vm-profiler/client-cli/src/test/java/com/redhat/thermostat/vm/profiler/client/cli/internal/ProfileVmCommandTest.java vm-profiler/client-swing/src/main/java/com/redhat/thermostat/vm/profiler/client/swing/internal/LocaleResources.java vm-profiler/client-swing/src/main/java/com/redhat/thermostat/vm/profiler/client/swing/internal/SwingVmProfileView.java vm-profiler/client-swing/src/main/java/com/redhat/thermostat/vm/profiler/client/swing/internal/VmProfileController.java vm-profiler/client-swing/src/main/java/com/redhat/thermostat/vm/profiler/client/swing/internal/VmProfileView.java vm-profiler/client-swing/src/main/resources/com/redhat/thermostat/vm/profiler/client/swing/internal/strings.properties vm-profiler/client-swing/src/test/java/com/redhat/thermostat/vm/profiler/client/swing/internal/VmProfileControllerTest.java vm-profiler/common/src/main/java/com/redhat/thermostat/vm/profiler/common/ProfileDAO.java vm-profiler/common/src/main/java/com/redhat/thermostat/vm/profiler/common/ProfileStatusChange.java vm-profiler/common/src/main/java/com/redhat/thermostat/vm/profiler/common/internal/Activator.java vm-profiler/common/src/main/java/com/redhat/thermostat/vm/profiler/common/internal/ProfileDAOImpl.java vm-profiler/common/src/main/java/com/redhat/thermostat/vm/profiler/common/internal/ProfileDAOImplCategoryRegistration.java vm-profiler/common/src/main/java/com/redhat/thermostat/vm/profiler/common/internal/ProfileDAOImplStatementDescriptorRegistration.java vm-profiler/common/src/test/java/com/redhat/thermostat/vm/profiler/common/internal/ProfileDAOImplCategoryRegistrationTest.java vm-profiler/common/src/test/java/com/redhat/thermostat/vm/profiler/common/internal/ProfileDAOImplStatementDescriptorRegistrationTest.java vm-profiler/common/src/test/java/com/redhat/thermostat/vm/profiler/common/internal/ProfileDAOImplTest.java
diffstat 21 files changed, 676 insertions(+), 79 deletions(-) [+]
line wrap: on
line diff
--- a/vm-profiler/agent/src/main/java/com/redhat/thermostat/vm/profiler/agent/internal/ProfileVmRequestReceiver.java	Fri Nov 28 16:36:17 2014 -0500
+++ b/vm-profiler/agent/src/main/java/com/redhat/thermostat/vm/profiler/agent/internal/ProfileVmRequestReceiver.java	Fri Nov 28 16:37:02 2014 -0500
@@ -58,6 +58,7 @@
 import com.redhat.thermostat.common.utils.LoggingUtils;
 import com.redhat.thermostat.vm.profiler.common.ProfileDAO;
 import com.redhat.thermostat.vm.profiler.common.ProfileRequest;
+import com.redhat.thermostat.vm.profiler.common.ProfileStatusChange;
 
 public class ProfileVmRequestReceiver implements RequestReceiver, VmStatusListener {
 
@@ -136,7 +137,7 @@
 
         switch (value) {
         case ProfileRequest.START_PROFILING:
-            return startProfiling(pid);
+            return startProfiling(vmId, pid);
         case ProfileRequest.STOP_PROFILING:
             return stopProfiling(vmId, pid, true);
         default:
@@ -155,11 +156,12 @@
         }
     }
 
-    private Response startProfiling(int pid) {
+    private Response startProfiling(String vmId, int pid) {
         logger.info("Starting profiling " + pid);
         try {
             profiler.startProfiling(pid);
             currentlyProfiledVms.add(pid);
+            dao.addStatus(new ProfileStatusChange(agentId, vmId, clock.getRealTimeMillis(), true));
             return OK;
         } catch (Exception e) {
             logger.log(Level.INFO, "start profiling failed", e);
@@ -177,6 +179,7 @@
             } else {
                 findAndUploadProfilingResultsStoredOnDisk(pid, uploader);
             }
+            dao.addStatus(new ProfileStatusChange(agentId, vmId, clock.getRealTimeMillis(), false));
             currentlyProfiledVms.remove((Integer) pid);
             return OK;
         } catch (Exception e) {
--- a/vm-profiler/agent/src/test/java/com/redhat/thermostat/vm/profiler/agent/internal/ProfileVmRequestReceiverTest.java	Fri Nov 28 16:36:17 2014 -0500
+++ b/vm-profiler/agent/src/test/java/com/redhat/thermostat/vm/profiler/agent/internal/ProfileVmRequestReceiverTest.java	Fri Nov 28 16:37:02 2014 -0500
@@ -42,6 +42,7 @@
 import static org.mockito.Matchers.isA;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
@@ -60,8 +61,8 @@
 import com.redhat.thermostat.common.command.Response.ResponseType;
 import com.redhat.thermostat.vm.profiler.agent.internal.ProfileVmRequestReceiver.ProfileUploaderCreator;
 import com.redhat.thermostat.vm.profiler.common.ProfileDAO;
-import com.redhat.thermostat.vm.profiler.common.ProfileInfo;
 import com.redhat.thermostat.vm.profiler.common.ProfileRequest;
+import com.redhat.thermostat.vm.profiler.common.ProfileStatusChange;
 
 public class ProfileVmRequestReceiverTest {
 
@@ -108,6 +109,8 @@
 
         assertEquals(ResponseType.OK, result.getType());
         verify(profiler).startProfiling(VM_PID);
+
+        verify(dao).addStatus(new ProfileStatusChange(AGENT_ID, VM_ID, TIMESTAMP, true));
     }
 
     @Test
@@ -126,6 +129,7 @@
 
         assertEquals(ResponseType.OK, result.getType());
         verify(profiler).stopProfiling(eq(VM_PID), isA(ProfileUploader.class));
+        verify(dao, times(2)).addStatus(isA(ProfileStatusChange.class));
     }
 
     @Test
@@ -136,6 +140,7 @@
         Request request = ProfileRequest.create(null, VM_ID, ProfileRequest.START_PROFILING);
         Response result = requestReceiver.receive(request);
         assertEquals(ResponseType.NOK, result.getType());
+        verify(dao, never()).addStatus(isA(ProfileStatusChange.class));
     }
 
     @Test
@@ -156,6 +161,7 @@
 
         verify(profiler, never()).stopProfiling(anyInt(), isA(ProfileUploader.class));
         verify(uploader).upload(TIMESTAMP, profilingResults);
+        verify(dao, times(2)).addStatus(isA(ProfileStatusChange.class));
 
         profilingResults.delete();
     }
--- a/vm-profiler/client-cli/src/main/java/com/redhat/thermostat/vm/profiler/client/cli/internal/LocaleResources.java	Fri Nov 28 16:36:17 2014 -0500
+++ b/vm-profiler/client-cli/src/main/java/com/redhat/thermostat/vm/profiler/client/cli/internal/LocaleResources.java	Fri Nov 28 16:37:02 2014 -0500
@@ -48,6 +48,10 @@
     COMMAND_EXPECTED,
     UNKNOWN_COMMAND,
     INTERRUPTED_WAITING_FOR_RESPONSE,
+    AGENT_NOT_FOUND,
+
+    STATUS_CURRENTLY_PROFILING,
+    STATUS_CURRENTLY_NOT_PROFILING,
 
     METHOD_PROFILE_HEADER_PERCENTAGE,
     METHOD_PROFILE_HEADER_TIME,
--- a/vm-profiler/client-cli/src/main/java/com/redhat/thermostat/vm/profiler/client/cli/internal/ProfileVmCommand.java	Fri Nov 28 16:36:17 2014 -0500
+++ b/vm-profiler/client-cli/src/main/java/com/redhat/thermostat/vm/profiler/client/cli/internal/ProfileVmCommand.java	Fri Nov 28 16:37:02 2014 -0500
@@ -63,6 +63,7 @@
 import com.redhat.thermostat.vm.profiler.client.core.ProfilingResultParser;
 import com.redhat.thermostat.vm.profiler.common.ProfileDAO;
 import com.redhat.thermostat.vm.profiler.common.ProfileRequest;
+import com.redhat.thermostat.vm.profiler.common.ProfileStatusChange;
 
 public class ProfileVmCommand extends AbstractCommand {
 
@@ -70,6 +71,7 @@
 
     private static final String START_ARGUMENT = "start";
     private static final String STOP_ARGUMENT = "stop";
+    private static final String STATUS_ARGUMENT = "status";
     private static final String SHOW_ARGUMENT = "show";
 
     @Override
@@ -88,8 +90,7 @@
 
         AgentInformation agentInfo = agentInfoDAO.getAgentInformation(args.getHost());
         if (agentInfo == null) {
-            ctx.getConsole().getError().println("error: agent '" + args.getHost().getAgentId() + "' not found'");
-            return;
+            throw new CommandException(translator.localize(LocaleResources.AGENT_NOT_FOUND, args.getHost().getAgentId()));
         }
 
         InetSocketAddress target = agentInfo.getRequestQueueAddress();
@@ -108,6 +109,9 @@
         case STOP_ARGUMENT:
             sendStopProfilingRequest(ctx.getConsole(), requestQueue, target, args.getVM().getVmId());
             break;
+        case STATUS_ARGUMENT:
+            showProfilingStatus(ctx.getConsole(), args.getVM());
+            break;
         case SHOW_ARGUMENT:
             showProfilingResults(ctx.getConsole(), args.getVM());
             break;
@@ -171,6 +175,22 @@
 
     }
 
+    private void showProfilingStatus(Console console, VmRef vm) {
+        ProfileDAO dao = getService(ProfileDAO.class);
+        ProfileStatusChange latest = dao.getLatestStatus(vm);
+        boolean profiling = false;
+        if (latest != null) {
+            profiling = latest.isStarted();
+        }
+        String message;
+        if (profiling) {
+            message = translator.localize(LocaleResources.STATUS_CURRENTLY_PROFILING).getContents();
+        } else {
+            message = translator.localize(LocaleResources.STATUS_CURRENTLY_NOT_PROFILING).getContents();
+        }
+        console.getOutput().println(message);
+    }
+
     private void showProfilingResults(Console console, VmRef vm) {
         ProfileDAO dao = getService(ProfileDAO.class);
         InputStream data = dao.loadLatestProfileData(vm);
--- a/vm-profiler/client-cli/src/main/resources/com/redhat/thermostat/vm/profiler/client/cli/internal/strings.properties	Fri Nov 28 16:36:17 2014 -0500
+++ b/vm-profiler/client-cli/src/main/resources/com/redhat/thermostat/vm/profiler/client/cli/internal/strings.properties	Fri Nov 28 16:37:02 2014 -0500
@@ -6,6 +6,10 @@
 COMMAND_EXPECTED = A valid subcommand is expected.
 UNKNOWN_COMMAND = Unknown command: {0}
 INTERRUPTED_WAITING_FOR_RESPONSE = Interrupted while waiting for a response from agent
+AGENT_NOT_FOUND = error: agent {0} not found
+
+STATUS_CURRENTLY_PROFILING = Currently profiling: Yes
+STATUS_CURRENTLY_NOT_PROFILING = Currently profiling: No
 
 METHOD_PROFILE_HEADER_PERCENTAGE = % Time
 METHOD_PROFILE_HEADER_TIME = Time (ms)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/vm-profiler/client-cli/src/test/java/com/redhat/thermostat/vm/profiler/client/cli/internal/ProfileVmCommandTest.java	Fri Nov 28 16:37:02 2014 -0500
@@ -0,0 +1,142 @@
+/*
+ * Copyright 2012-2014 Red Hat, Inc.
+ *
+ * This file is part of Thermostat.
+ *
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ *
+ * Thermostat is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ *
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.vm.profiler.client.cli.internal;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import com.redhat.thermostat.client.cli.internal.LocaleResources;
+import com.redhat.thermostat.client.command.RequestQueue;
+import com.redhat.thermostat.common.cli.CommandContext;
+import com.redhat.thermostat.common.cli.CommandException;
+import com.redhat.thermostat.common.cli.SimpleArguments;
+import com.redhat.thermostat.shared.locale.Translate;
+import com.redhat.thermostat.storage.core.HostRef;
+import com.redhat.thermostat.storage.core.VmRef;
+import com.redhat.thermostat.storage.dao.AgentInfoDAO;
+import com.redhat.thermostat.storage.dao.VmInfoDAO;
+import com.redhat.thermostat.storage.model.AgentInformation;
+import com.redhat.thermostat.test.TestCommandContextFactory;
+import com.redhat.thermostat.vm.profiler.common.ProfileDAO;
+import com.redhat.thermostat.vm.profiler.common.ProfileStatusChange;
+
+public class ProfileVmCommandTest {
+
+    private static final Translate<LocaleResources> translator = LocaleResources.createLocalizer();
+
+    private static final String AGENT_ID = "some-agent";
+    private static final String VM_ID = "some-vm";
+    private static final long SOME_TIMESTAMP = 99;
+
+    /* taken from HostVMArguments: */
+    private static final HostRef AGENT = new HostRef(AGENT_ID, "dummy");
+    private static final VmRef VM = new VmRef(AGENT, VM_ID, -1, "dummy");
+
+    private TestCommandContextFactory cmdCtxFactory;
+
+    private AgentInfoDAO agentsDao;
+    private VmInfoDAO vmsDao;
+    private RequestQueue queue;
+    private ProfileDAO profileDao;
+
+    private ProfileVmCommand cmd;
+
+    @Before
+    public void setUp() {
+        cmdCtxFactory = new TestCommandContextFactory();
+
+        agentsDao = mock(AgentInfoDAO.class);
+        vmsDao = mock(VmInfoDAO.class);
+        queue = mock(RequestQueue.class);
+        profileDao = mock(ProfileDAO.class);
+
+        cmd = new ProfileVmCommand();
+        cmd.setAgentInfoDAO(agentsDao);
+        cmd.setVmInfoDAO(vmsDao);
+        cmd.setRequestQueue(queue);
+        cmd.setProfileDAO(profileDao);
+    }
+
+    @Test (expected=CommandException.class)
+    public void needsSubCommand() throws Exception {
+        SimpleArguments args = new SimpleArguments();
+        args.addArgument("hostId", AGENT_ID);
+        args.addArgument("vmId", VM_ID);
+        CommandContext ctx = cmdCtxFactory.createContext(args);
+
+        cmd.run(ctx);
+    }
+
+    @Test
+    public void statusSubCommandShowsNotProfilingWhenNoInformationAvailable() throws Exception {
+        AgentInformation agentInfo = mock(AgentInformation.class);
+        when(agentsDao.getAgentInformation(AGENT)).thenReturn(agentInfo);
+
+        SimpleArguments args = new SimpleArguments();
+        args.addArgument("hostId", AGENT_ID);
+        args.addArgument("vmId", VM_ID);
+        args.addNonOptionArgument("status");
+        CommandContext ctx = cmdCtxFactory.createContext(args);
+
+        cmd.run(ctx);
+
+        assertEquals("Currently profiling: No\n", cmdCtxFactory.getOutput());
+    }
+
+    @Test
+    public void statusSubCommandShowsCurrentProfilingStatus() throws Exception {
+        AgentInformation agentInfo = mock(AgentInformation.class);
+        when(agentsDao.getAgentInformation(AGENT)).thenReturn(agentInfo);
+
+        ProfileStatusChange status = new ProfileStatusChange(AGENT_ID, VM_ID, SOME_TIMESTAMP, true);
+        when(profileDao.getLatestStatus(VM)).thenReturn(status);
+
+        SimpleArguments args = new SimpleArguments();
+        args.addArgument("hostId", AGENT_ID);
+        args.addArgument("vmId", VM_ID);
+        args.addNonOptionArgument("status");
+        CommandContext ctx = cmdCtxFactory.createContext(args);
+
+        cmd.run(ctx);
+
+        assertEquals("Currently profiling: Yes\n", cmdCtxFactory.getOutput());
+    }
+}
--- a/vm-profiler/client-swing/src/main/java/com/redhat/thermostat/vm/profiler/client/swing/internal/LocaleResources.java	Fri Nov 28 16:36:17 2014 -0500
+++ b/vm-profiler/client-swing/src/main/java/com/redhat/thermostat/vm/profiler/client/swing/internal/LocaleResources.java	Fri Nov 28 16:37:02 2014 -0500
@@ -43,6 +43,9 @@
     PROFILER_TAB_NAME,
 
     PROFILER_HEADING,
+
+    PROFILER_CURRENT_STATUS_ACTIVE,
+    PROFILER_CURRENT_STATUS_INACTIVE,
     START_PROFILING,
     STOP_PROFILING,
 
--- a/vm-profiler/client-swing/src/main/java/com/redhat/thermostat/vm/profiler/client/swing/internal/SwingVmProfileView.java	Fri Nov 28 16:36:17 2014 -0500
+++ b/vm-profiler/client-swing/src/main/java/com/redhat/thermostat/vm/profiler/client/swing/internal/SwingVmProfileView.java	Fri Nov 28 16:37:02 2014 -0500
@@ -38,6 +38,8 @@
 
 import java.awt.BorderLayout;
 import java.awt.Component;
+import java.awt.GridBagConstraints;
+import java.awt.GridBagLayout;
 import java.util.Date;
 import java.util.List;
 import java.util.Vector;
@@ -45,19 +47,24 @@
 
 import javax.swing.DefaultListCellRenderer;
 import javax.swing.DefaultListModel;
+import javax.swing.JComponent;
+import javax.swing.JFrame;
+import javax.swing.JLabel;
 import javax.swing.JList;
+import javax.swing.JPanel;
 import javax.swing.JScrollPane;
 import javax.swing.JSplitPane;
 import javax.swing.JTable;
+import javax.swing.JToggleButton;
 import javax.swing.ListSelectionModel;
+import javax.swing.SwingUtilities;
 import javax.swing.SwingWorker;
+import javax.swing.WindowConstants;
 import javax.swing.event.ListSelectionEvent;
 import javax.swing.event.ListSelectionListener;
 import javax.swing.table.DefaultTableModel;
 
-import com.redhat.thermostat.client.swing.IconResource;
 import com.redhat.thermostat.client.swing.SwingComponent;
-import com.redhat.thermostat.client.swing.components.ActionToggleButton;
 import com.redhat.thermostat.client.swing.components.HeaderPanel;
 import com.redhat.thermostat.client.swing.components.ThermostatTable;
 import com.redhat.thermostat.client.swing.experimental.ComponentVisibilityNotifier;
@@ -73,18 +80,20 @@
 
     private static final double SPLIT_PANE_RATIO = 0.3;
 
-    private final CopyOnWriteArrayList<ActionListener<ProfileAction>> listeners =
-            new CopyOnWriteArrayList<>();
+    private final CopyOnWriteArrayList<ActionListener<ProfileAction>> listeners = new CopyOnWriteArrayList<>();
 
     private HeaderPanel mainContainer;
 
-    private ActionToggleButton startStopProfilingButton;
+    private JToggleButton startButton;
+    private JToggleButton stopButton;
 
     private DefaultListModel<Profile> listModel;
     private JList<Profile> profileList;
 
     private DefaultTableModel tableModel;
 
+    private JLabel currentStatusLabel;
+
     static class ProfileItemRenderer extends DefaultListCellRenderer {
         @Override
         public Component getListCellRendererComponent(JList<?> list,
@@ -104,27 +113,58 @@
     public SwingVmProfileView() {
         listModel = new DefaultListModel<>();
 
-        startStopProfilingButton = new ActionToggleButton(
-                IconResource.RECORD.getIcon());
-        updateProfilingButtonStatus(false);
+        mainContainer = new HeaderPanel(translator.localize(LocaleResources.PROFILER_HEADING));
+        new ComponentVisibilityNotifier().initialize(mainContainer, notifier);
+
+        JPanel contentContainer = new JPanel(new BorderLayout());
+        mainContainer.setContent(contentContainer);
+
+        JComponent actionsPanel = createActionsPanel();
+        contentContainer.add(actionsPanel, BorderLayout.PAGE_START);
+
+        JComponent profilingResultsPanel = createInformationPanel();
+        contentContainer.add(profilingResultsPanel, BorderLayout.CENTER);
+    }
 
-        startStopProfilingButton.addActionListener(new java.awt.event.ActionListener() {
+    private JPanel createActionsPanel() {
+        GridBagLayout layout = new GridBagLayout();
+        JPanel actionsPanel = new JPanel(layout);
+
+        GridBagConstraints constraints = new GridBagConstraints();
+        constraints.fill = GridBagConstraints.HORIZONTAL;
+        constraints.weightx = 1.0;
+
+        currentStatusLabel = new JLabel("Current Status: {0}");
+        actionsPanel.add(currentStatusLabel, constraints);
+
+        constraints.fill = GridBagConstraints.NONE;
+        constraints.weightx = 0.0;
+        startButton = new JToggleButton(translator.localize(LocaleResources.START_PROFILING).getContents());
+        startButton.addActionListener(new java.awt.event.ActionListener() {
             @Override
             public void actionPerformed(java.awt.event.ActionEvent e) {
-                if (startStopProfilingButton.isSelected()) {
-                    updateProfilingButtonStatus(true);
+                JToggleButton button = (JToggleButton) e.getSource();
+                if (button.isSelected()) {
                     fireProfileAction(ProfileAction.START_PROFILING);
-                } else {
-                    updateProfilingButtonStatus(false);
+                }
+            }
+        });
+        actionsPanel.add(startButton, constraints);
+        stopButton = new JToggleButton(translator.localize(LocaleResources.STOP_PROFILING).getContents());
+        stopButton.addActionListener(new java.awt.event.ActionListener() {
+            @Override
+            public void actionPerformed(java.awt.event.ActionEvent e) {
+                JToggleButton button = (JToggleButton) e.getSource();
+                if (button.isSelected()) {
                     fireProfileAction(ProfileAction.STOP_PROFILING);
                 }
             }
         });
+        actionsPanel.add(stopButton, constraints);
+        return actionsPanel;
+    }
 
-        mainContainer = new HeaderPanel(translator.localize(LocaleResources.PROFILER_HEADING));
-        mainContainer.addToolBarButton(startStopProfilingButton);
-        new ComponentVisibilityNotifier().initialize(mainContainer, notifier);
-
+    private JComponent createInformationPanel() {
         profileList = new JList<>(listModel);
         profileList.setCellRenderer(new ProfileItemRenderer());
         profileList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
@@ -156,7 +196,7 @@
         splitPane.setDividerLocation(SPLIT_PANE_RATIO);
         splitPane.setResizeWeight(0.5);
 
-        mainContainer.add(splitPane, BorderLayout.CENTER);
+        return splitPane;
     }
 
     @Override
@@ -183,16 +223,40 @@
     }
 
     @Override
-    public void setCurrentlyProfiling(boolean currentlyProfiling) {
-        updateProfilingButtonStatus(currentlyProfiling);
+    public void enableStartProfiling(final boolean start) {
+        SwingUtilities.invokeLater(new Runnable() {
+            @Override
+            public void run() {
+                startButton.setEnabled(start);
+            }
+        });
     }
 
-    private void updateProfilingButtonStatus(boolean currentlyProfiling) {
-        if (currentlyProfiling) {
-            startStopProfilingButton.setText(translator.localize(LocaleResources.STOP_PROFILING).getContents());
-        } else {
-            startStopProfilingButton.setText(translator.localize(LocaleResources.START_PROFILING).getContents());
-        }
+    @Override
+    public void enableStopProfiling(final boolean stop) {
+        SwingUtilities.invokeLater(new Runnable() {
+            @Override
+            public void run() {
+                stopButton.setEnabled(stop);
+            }
+        });
+    }
+
+    @Override
+    public void setProfilingStatus(final String text, final boolean active) {
+        SwingUtilities.invokeLater(new Runnable() {
+            @Override
+            public void run() {
+                currentStatusLabel.setText(text);
+                if (active) {
+                    startButton.setSelected(true);
+                    stopButton.setSelected(false);
+                } else {
+                    startButton.setSelected(false);
+                    stopButton.setSelected(true);
+                }
+            }
+        });
     }
 
     @Override
@@ -232,4 +296,17 @@
         return mainContainer;
     }
 
+    public static void main(String[] args) {
+        SwingUtilities.invokeLater(new Runnable() {
+            @Override
+            public void run() {
+                JFrame window = new JFrame();
+                SwingVmProfileView view = new SwingVmProfileView();
+                window.add(view.getUiComponent());
+                window.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
+                window.pack();
+                window.setVisible(true);
+            }
+        });
+    }
 }
--- a/vm-profiler/client-swing/src/main/java/com/redhat/thermostat/vm/profiler/client/swing/internal/VmProfileController.java	Fri Nov 28 16:36:17 2014 -0500
+++ b/vm-profiler/client-swing/src/main/java/com/redhat/thermostat/vm/profiler/client/swing/internal/VmProfileController.java	Fri Nov 28 16:37:02 2014 -0500
@@ -55,6 +55,8 @@
 import com.redhat.thermostat.common.Timer;
 import com.redhat.thermostat.common.Timer.SchedulingType;
 import com.redhat.thermostat.common.command.Request;
+import com.redhat.thermostat.common.command.RequestResponseListener;
+import com.redhat.thermostat.common.command.Response;
 import com.redhat.thermostat.common.model.Range;
 import com.redhat.thermostat.shared.locale.LocalizedString;
 import com.redhat.thermostat.shared.locale.Translate;
@@ -67,6 +69,7 @@
 import com.redhat.thermostat.vm.profiler.common.ProfileDAO;
 import com.redhat.thermostat.vm.profiler.common.ProfileInfo;
 import com.redhat.thermostat.vm.profiler.common.ProfileRequest;
+import com.redhat.thermostat.vm.profiler.common.ProfileStatusChange;
 
 public class VmProfileController implements InformationServiceController<VmRef> {
 
@@ -84,6 +87,10 @@
 
     private Clock clock;
 
+    private boolean profilingStartOrStopRequested = false;
+
+    private ProfileStatusChange previousStatus;
+
     public VmProfileController(ApplicationService service,
             AgentInfoDAO agentInfoDao, ProfileDAO dao,
             RequestQueue queue,
@@ -94,7 +101,7 @@
     VmProfileController(ApplicationService service,
             AgentInfoDAO agentInfoDao, ProfileDAO dao,
             RequestQueue queue, Clock clock,
-            VmProfileView view, VmRef vm) {
+            final VmProfileView view, VmRef vm) {
         this.service = service;
         this.agentInfoDao = agentInfoDao;
         this.profileDao = dao;
@@ -112,8 +119,10 @@
         updater.setAction(new Runnable() {
             @Override
             public void run() {
+                updateViewWithCurrentProfilingStatus();
                 updateViewWithProfiledRuns();
             }
+
         });
 
         view.addActionListener(new ActionListener<BasicView.Action>() {
@@ -138,10 +147,10 @@
                 ProfileAction id = actionEvent.getActionId();
                 switch (id) {
                 case START_PROFILING:
-                    sendProfilingRequest(true);
+                    startProfiling(view);
                     break;
                 case STOP_PROFILING:
-                    sendProfilingRequest(false);
+                    stopProfiling(view);
                     break;
                 case PROFILE_SELECTED:
                     updateViewWithProfileRunData();
@@ -150,14 +159,83 @@
                     throw new AssertionError("Unknown event: " + id);
                 }
             }
+
         });
     }
 
-    private void sendProfilingRequest(boolean start) {
+    private void startProfiling(final VmProfileView view) {
+        disableViewControlsAndSendRequest(view, true);
+    }
+
+    private void stopProfiling(final VmProfileView view) {
+        disableViewControlsAndSendRequest(view, false);
+    }
+
+    private void disableViewControlsAndSendRequest(VmProfileView view, boolean start) {
+        // disable the UI until we get a update in storage
+        view.enableStartProfiling(false);
+        view.enableStopProfiling(false);
+
+        sendProfilingRequest(start);
+    }
+
+    private void sendProfilingRequest(final boolean start) {
         InetSocketAddress address = agentInfoDao.getAgentInformation(vm.getHostRef()).getRequestQueueAddress();
         String action = start ? ProfileRequest.START_PROFILING : ProfileRequest.STOP_PROFILING;
         Request req = ProfileRequest.create(address, vm.getVmId(), action);
+        req.addListener(new RequestResponseListener() {
+            @Override
+            public void fireComplete(Request request, Response response) {
+                switch (response.getType()) {
+                case OK:
+                    updateViewWithCurrentProfilingStatus();
+                    break;
+                default:
+                    // FIXME show message to user
+
+                    profilingStartOrStopRequested = false;
+                    break;
+                }
+            }
+        });
         queue.putRequest(req);
+        profilingStartOrStopRequested = true;
+    }
+
+    private void updateViewWithCurrentProfilingStatus() {
+        boolean currentlyActive = false;
+
+        ProfileStatusChange currentStatus = profileDao.getLatestStatus(vm);
+        if (currentStatus != null) {
+            currentlyActive = currentStatus.isStarted();
+        }
+
+        String message;
+        if (currentlyActive) {
+            message = translator.localize(LocaleResources.PROFILER_CURRENT_STATUS_ACTIVE).getContents();
+        } else {
+            message = translator.localize(LocaleResources.PROFILER_CURRENT_STATUS_INACTIVE).getContents();
+        }
+
+        if (profilingStartOrStopRequested) {
+            boolean statusChanged = (previousStatus == null && currentStatus != null)
+                    || (currentStatus != null && !(currentStatus.equals(previousStatus)));
+            if (statusChanged) {
+                view.enableStartProfiling(!currentlyActive);
+                view.enableStopProfiling(currentlyActive);
+
+                view.setProfilingStatus(message, currentlyActive);
+
+                profilingStartOrStopRequested = false;
+            }
+        } else {
+            view.enableStartProfiling(!currentlyActive);
+            view.enableStopProfiling(currentlyActive);
+
+            view.setProfilingStatus(message, currentlyActive);
+        }
+
+        previousStatus = currentStatus;
     }
 
     private void updateViewWithProfiledRuns() {
--- a/vm-profiler/client-swing/src/main/java/com/redhat/thermostat/vm/profiler/client/swing/internal/VmProfileView.java	Fri Nov 28 16:36:17 2014 -0500
+++ b/vm-profiler/client-swing/src/main/java/com/redhat/thermostat/vm/profiler/client/swing/internal/VmProfileView.java	Fri Nov 28 16:37:02 2014 -0500
@@ -65,7 +65,17 @@
 
     public abstract void removeProfileActionlistener(ActionListener<ProfileAction> listener);
 
-    public abstract void setCurrentlyProfiling(boolean profiling);
+    /*
+     * Because of the latency between asking for starting profiling and actually
+     * starting profiling, we use a lot more states than 'enabled/disabled' for
+     * indicating profiling in the UI
+     */
+
+    /** Enable (or disable) UI that starts profiling */
+    public abstract void enableStartProfiling(boolean start);
+    /** Enable (or disable) UI that stops profiling */
+    public abstract void enableStopProfiling(boolean stop);
+    public abstract void setProfilingStatus(String text, boolean enabled);
 
     public abstract void setAvailableProfilingRuns(List<Profile> data);
 
--- a/vm-profiler/client-swing/src/main/resources/com/redhat/thermostat/vm/profiler/client/swing/internal/strings.properties	Fri Nov 28 16:36:17 2014 -0500
+++ b/vm-profiler/client-swing/src/main/resources/com/redhat/thermostat/vm/profiler/client/swing/internal/strings.properties	Fri Nov 28 16:37:02 2014 -0500
@@ -2,6 +2,8 @@
 
 PROFILER_HEADING = JVM Profiler
 
+PROFILER_CURRENT_STATUS_ACTIVE = Currently profiling: yes
+PROFILER_CURRENT_STATUS_INACTIVE = Currently profiling: no
 START_PROFILING = Start Profiling
 STOP_PROFILING = Stop Profiling
 
--- a/vm-profiler/client-swing/src/test/java/com/redhat/thermostat/vm/profiler/client/swing/internal/VmProfileControllerTest.java	Fri Nov 28 16:36:17 2014 -0500
+++ b/vm-profiler/client-swing/src/test/java/com/redhat/thermostat/vm/profiler/client/swing/internal/VmProfileControllerTest.java	Fri Nov 28 16:37:02 2014 -0500
@@ -36,7 +36,7 @@
 
 package com.redhat.thermostat.vm.profiler.client.swing.internal;
 
-import static org.junit.Assert.*;
+import static org.junit.Assert.assertEquals;
 import static org.mockito.Matchers.isA;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
@@ -44,7 +44,6 @@
 import static org.mockito.Mockito.when;
 
 import java.io.ByteArrayInputStream;
-import java.net.InetAddress;
 import java.net.InetSocketAddress;
 import java.nio.charset.StandardCharsets;
 import java.util.Arrays;
@@ -75,6 +74,7 @@
 import com.redhat.thermostat.vm.profiler.common.ProfileDAO;
 import com.redhat.thermostat.vm.profiler.common.ProfileInfo;
 import com.redhat.thermostat.vm.profiler.common.ProfileRequest;
+import com.redhat.thermostat.vm.profiler.common.ProfileStatusChange;
 
 public class VmProfileControllerTest {
 
@@ -85,6 +85,9 @@
     private static final String VM_ID = "some-vm-id";
     private static final String PROFILE_ID = "some-profile-id";
 
+    private static final long SOME_TIMESTAMP = 1000000000;
+    private static final long PROFILE_TIMESTAMP = SOME_TIMESTAMP - 100;
+
     private Timer timer;
     private ApplicationService appService;
     private AgentInfoDAO agentInfoDao;
@@ -97,7 +100,6 @@
     private VmProfileController controller;
     private HostRef agent;
 
-
     @Before
     public void setUp() {
         timer = mock(Timer.class);
@@ -156,9 +158,6 @@
 
     @Test
     public void timerUpdatesView() throws Exception {
-        final long SOME_TIMESTAMP = 1000000000;
-        final long PROFILE_TIMESTAMP = SOME_TIMESTAMP - 100;
-
         when(clock.getRealTimeMillis()).thenReturn(SOME_TIMESTAMP);
         controller = createController();
 
@@ -171,6 +170,9 @@
                 new Range<>(SOME_TIMESTAMP - TimeUnit.DAYS.toMillis(1) , SOME_TIMESTAMP)))
             .thenReturn(Arrays.asList(profile));
 
+        ProfileStatusChange status = new ProfileStatusChange(AGENT_ID, VM_ID, PROFILE_TIMESTAMP, false);
+        when(profileDao.getLatestStatus(vm)).thenReturn(status);
+
         Runnable runnable = runnableCaptor.getValue();
         runnable.run();
 
@@ -179,6 +181,10 @@
         List<Profile> resultList = listCaptor.getValue();
         assertEquals(1, resultList.size());
         assertEquals(PROFILE_TIMESTAMP, resultList.get(0).timeStamp);
+
+        verify(view).setProfilingStatus("Currently profiling: no", false);
+        verify(view).enableStartProfiling(true);
+        verify(view).enableStopProfiling(false);
     }
 
     @Test
@@ -190,6 +196,9 @@
 
         listenerCaptor.getValue().actionPerformed(new ActionEvent<>(view, ProfileAction.START_PROFILING));
 
+        verify(view).enableStartProfiling(false);
+        verify(view).enableStopProfiling(false);
+
         ArgumentCaptor<Request> requestCaptor = ArgumentCaptor.forClass(Request.class);
         verify(queue).putRequest(requestCaptor.capture());
         Request expectedRequest = ProfileRequest.create(AGENT_ADDRESS, VM_ID, ProfileRequest.START_PROFILING);
@@ -198,6 +207,29 @@
     }
 
     @Test
+    public void startProfilingWaitsForDaoResultToEnableViewControls() {
+        controller = createController();
+
+        ArgumentCaptor<ActionListener> listenerCaptor = ArgumentCaptor.forClass(ActionListener.class);
+        verify(view).addProfileActionListener(listenerCaptor.capture());
+
+        listenerCaptor.getValue().actionPerformed(new ActionEvent<>(view, ProfileAction.START_PROFILING));
+
+        verify(view).enableStartProfiling(false);
+        verify(view).enableStopProfiling(false);
+
+        ProfileStatusChange status = new ProfileStatusChange(AGENT_ID, VM_ID, PROFILE_TIMESTAMP, true);
+        when(profileDao.getLatestStatus(vm)).thenReturn(status);
+
+        ArgumentCaptor<Runnable> runnableCaptor = ArgumentCaptor.forClass(Runnable.class);
+        verify(timer).setAction(runnableCaptor.capture());
+
+        runnableCaptor.getValue().run();
+
+        verify(view).enableStopProfiling(true);
+    }
+
+    @Test
     public void stopProfilingWorks() throws Exception {
         controller = createController();
 
@@ -215,10 +247,27 @@
         assertRequestEquals(actualRequest, expectedRequest);
     }
 
-    private void assertRequestEquals(Request actual, Request expected) {
-        assertEquals(expected.getParameterNames(), actual.getParameterNames());
-        assertEquals(expected.getReceiver(), actual.getReceiver());
-        assertEquals(expected.getType(), actual.getType());
+    @Test
+    public void stopProfilingWaitsForDaoResultToEnableViewControls() {
+        controller = createController();
+
+        ArgumentCaptor<ActionListener> listenerCaptor = ArgumentCaptor.forClass(ActionListener.class);
+        verify(view).addProfileActionListener(listenerCaptor.capture());
+
+        listenerCaptor.getValue().actionPerformed(new ActionEvent<>(view, ProfileAction.STOP_PROFILING));
+
+        verify(view).enableStartProfiling(false);
+        verify(view).enableStopProfiling(false);
+
+        ProfileStatusChange status = new ProfileStatusChange(AGENT_ID, VM_ID, PROFILE_TIMESTAMP, false);
+        when(profileDao.getLatestStatus(vm)).thenReturn(status);
+
+        ArgumentCaptor<Runnable> runnableCaptor = ArgumentCaptor.forClass(Runnable.class);
+        verify(timer).setAction(runnableCaptor.capture());
+
+        runnableCaptor.getValue().run();
+
+        verify(view).enableStartProfiling(true);
     }
 
     @Test
@@ -243,4 +292,11 @@
     private VmProfileController createController() {
         return new VmProfileController(appService, agentInfoDao, profileDao, queue, clock, view, vm);
     }
+
+    private void assertRequestEquals(Request actual, Request expected) {
+        assertEquals(expected.getParameterNames(), actual.getParameterNames());
+        assertEquals(expected.getReceiver(), actual.getReceiver());
+        assertEquals(expected.getType(), actual.getType());
+    }
+
 }
--- a/vm-profiler/common/src/main/java/com/redhat/thermostat/vm/profiler/common/ProfileDAO.java	Fri Nov 28 16:36:17 2014 -0500
+++ b/vm-profiler/common/src/main/java/com/redhat/thermostat/vm/profiler/common/ProfileDAO.java	Fri Nov 28 16:37:02 2014 -0500
@@ -55,5 +55,9 @@
     /** @return {@code null} if no data is available */
     InputStream loadLatestProfileData(VmRef vm);
 
+    void addStatus(ProfileStatusChange change);
+
+    /** @return {@code null} if no data is available */
+    ProfileStatusChange getLatestStatus(VmRef vm);
 
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/vm-profiler/common/src/main/java/com/redhat/thermostat/vm/profiler/common/ProfileStatusChange.java	Fri Nov 28 16:37:02 2014 -0500
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2012-2014 Red Hat, Inc.
+ *
+ * This file is part of Thermostat.
+ *
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ *
+ * Thermostat is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ *
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.vm.profiler.common;
+
+import java.util.Objects;
+
+import com.redhat.thermostat.storage.core.Entity;
+import com.redhat.thermostat.storage.core.Persist;
+import com.redhat.thermostat.storage.model.BasePojo;
+import com.redhat.thermostat.storage.model.TimeStampedPojo;
+
+/** Represents a change in profiling status. Either a START or a STOP. */
+@Entity
+public class ProfileStatusChange extends BasePojo implements TimeStampedPojo {
+
+    private String vmId;
+    private long timeStamp;
+    private boolean started;
+
+    public ProfileStatusChange(String agentId, String vmId, long timeStamp, boolean started) {
+        super(agentId);
+        this.vmId = vmId;
+        this.timeStamp = timeStamp;
+        this.started = started;
+    }
+
+    /* for deserialization */
+    public ProfileStatusChange() {
+        super(null); // fixed up by the deserializer
+    }
+
+    @Persist
+    public void setVmId(String vmId) {
+        this.vmId = vmId;
+    }
+
+    @Persist
+    public String getVmId() {
+        return this.vmId;
+    }
+
+    @Persist
+    public void setTimeStamp(long timeStamp) {
+        this.timeStamp = timeStamp;
+    }
+
+    @Persist
+    @Override
+    public long getTimeStamp() {
+        return this.timeStamp;
+    }
+
+    @Persist
+    public void setStarted(boolean started) {
+        this.started = started;
+    }
+
+    @Persist
+    public boolean isStarted() {
+        return this.started;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (super.equals(obj)) {
+            ProfileStatusChange other = (ProfileStatusChange) obj;
+            return Objects.equals(this.vmId, other.vmId)
+                    && Objects.equals(this.timeStamp, other.timeStamp)
+                    && Objects.equals(this.started, other.started);
+        }
+        return false;
+    }
+}
--- a/vm-profiler/common/src/main/java/com/redhat/thermostat/vm/profiler/common/internal/Activator.java	Fri Nov 28 16:36:17 2014 -0500
+++ b/vm-profiler/common/src/main/java/com/redhat/thermostat/vm/profiler/common/internal/Activator.java	Fri Nov 28 16:37:02 2014 -0500
@@ -40,9 +40,7 @@
 
 import org.osgi.framework.BundleActivator;
 import org.osgi.framework.BundleContext;
-import org.osgi.framework.ServiceReference;
 import org.osgi.framework.ServiceRegistration;
-import org.osgi.util.tracker.ServiceTracker;
 
 import com.redhat.thermostat.common.MultipleServiceTracker;
 import com.redhat.thermostat.storage.core.Storage;
--- a/vm-profiler/common/src/main/java/com/redhat/thermostat/vm/profiler/common/internal/ProfileDAOImpl.java	Fri Nov 28 16:36:17 2014 -0500
+++ b/vm-profiler/common/src/main/java/com/redhat/thermostat/vm/profiler/common/internal/ProfileDAOImpl.java	Fri Nov 28 16:37:02 2014 -0500
@@ -53,8 +53,10 @@
 import com.redhat.thermostat.storage.core.Storage;
 import com.redhat.thermostat.storage.core.VmRef;
 import com.redhat.thermostat.storage.core.VmTimeIntervalPojoListGetter;
+import com.redhat.thermostat.storage.model.BasePojo;
 import com.redhat.thermostat.vm.profiler.common.ProfileDAO;
 import com.redhat.thermostat.vm.profiler.common.ProfileInfo;
+import com.redhat.thermostat.vm.profiler.common.ProfileStatusChange;
 
 public class ProfileDAOImpl implements ProfileDAO {
 
@@ -62,42 +64,63 @@
 
     private static final Key<String> KEY_PROFILE_ID = new Key<>("profileId");
 
-    static final Category<ProfileInfo> CATEGORY = new Category<>(
+    static final Category<ProfileInfo> PROFILE_INFO_CATEGORY = new Category<>(
             "profile-info",
             ProfileInfo.class,
             Key.AGENT_ID, Key.VM_ID, Key.TIMESTAMP, KEY_PROFILE_ID);
 
-    static final String DESC_ADD_PROFILE_INFO = ""
-            + "ADD " + CATEGORY.getName() + " SET "
+    static final String PROFILE_INFO_DESC_ADD = ""
+            + "ADD " + PROFILE_INFO_CATEGORY.getName() + " SET "
             + " '" + Key.AGENT_ID.getName() + "' = ?s ,"
             + " '" + Key.VM_ID.getName() + "' = ?s ,"
             + " '" + Key.TIMESTAMP.getName() + "' = ?l ,"
             + " '" + KEY_PROFILE_ID.getName() + "' = ?s";
 
-    static final String DESC_QUERY_LATEST = "QUERY "
-            + CATEGORY.getName() + " WHERE '"
+    static final String PROFILE_INFO_DESC_QUERY_LATEST = "QUERY "
+            + PROFILE_INFO_CATEGORY.getName() + " WHERE '"
             + Key.AGENT_ID.getName() + "' = ?s AND '"
             + Key.VM_ID.getName() + "' = ?s SORT '"
             + Key.TIMESTAMP.getName() + "' DSC LIMIT 1";
 
-    static final String DESC_QUERY_BY_ID = "QUERY "
-            + CATEGORY.getName() + " WHERE '"
+    static final String PROFILE_INFO_DESC_QUERY_BY_ID = "QUERY "
+            + PROFILE_INFO_CATEGORY.getName() + " WHERE '"
             + Key.AGENT_ID.getName() + "' = ?s AND '"
             + Key.VM_ID.getName() + "' = ?s AND '"
             + Key.TIMESTAMP.getName() + "' = ?s LIMIT 1";
 
     // internal information of VmTimeIntervalPojoListGetter being leaked :(
-    static final String DESC_INTERVAL_QUERY = String.format(
-            VmTimeIntervalPojoListGetter.VM_INTERVAL_QUERY_FORMAT, ProfileDAOImpl.CATEGORY.getName());
+    static final String PROFILE_INFO_DESC_INTERVAL_QUERY = String.format(
+            VmTimeIntervalPojoListGetter.VM_INTERVAL_QUERY_FORMAT, ProfileDAOImpl.PROFILE_INFO_CATEGORY.getName());
+
+    private static final Key<Boolean> KEY_PROFILE_STARTED = new Key<>("started");
+
+    static final Category<ProfileStatusChange> PROFILE_STATUS_CATEGORY = new Category<>(
+            "profile-status",
+            ProfileStatusChange.class,
+            Key.AGENT_ID, Key.VM_ID, Key.TIMESTAMP, KEY_PROFILE_STARTED);
+
+    static final String PROFILE_STATUS_DESC_ADD = ""
+            + "ADD " + PROFILE_STATUS_CATEGORY.getName() + " SET "
+            + " '" + Key.AGENT_ID.getName() + "' = ?s ,"
+            + " '" + Key.VM_ID.getName() + "' = ?s ,"
+            + " '" + Key.TIMESTAMP.getName() + "' = ?l ,"
+            + " '" + KEY_PROFILE_STARTED.getName() + "' = ?b";
+
+    static final String PROFILE_STATUS_DESC_QUERY_LATEST = "QUERY "
+            + PROFILE_STATUS_CATEGORY.getName() + " WHERE '"
+            + Key.AGENT_ID.getName() + "' = ?s AND '"
+            + Key.VM_ID.getName() + "' = ?s SORT '"
+            + Key.TIMESTAMP.getName() + "' DSC LIMIT 1";
 
     private final Storage storage;
     private final VmTimeIntervalPojoListGetter<ProfileInfo> getter;
 
     public ProfileDAOImpl(Storage storage) {
         this.storage = storage;
-        this.storage.registerCategory(CATEGORY);
+        this.storage.registerCategory(PROFILE_INFO_CATEGORY);
+        this.storage.registerCategory(PROFILE_STATUS_CATEGORY);
 
-        this.getter = new VmTimeIntervalPojoListGetter<>(storage, CATEGORY);
+        this.getter = new VmTimeIntervalPojoListGetter<>(storage, PROFILE_INFO_CATEGORY);
     }
 
     @Override
@@ -107,7 +130,7 @@
     }
 
     private void addProfileInfoToStorage(ProfileInfo info) {
-        StatementDescriptor<ProfileInfo> desc = new StatementDescriptor<>(CATEGORY, DESC_ADD_PROFILE_INFO);
+        StatementDescriptor<ProfileInfo> desc = new StatementDescriptor<>(PROFILE_INFO_CATEGORY, PROFILE_INFO_DESC_ADD);
         PreparedStatement<ProfileInfo> prepared;
         try {
             prepared = storage.prepareStatement(desc);
@@ -131,24 +154,58 @@
 
     @Override
     public InputStream loadProfileDataById(VmRef vm, String profileId) {
-        // TODO should we check whether this profileId is valid by querying the DB first?
         return getProfileData(profileId);
     }
 
     @Override
     public InputStream loadLatestProfileData(VmRef vm) {
-        StatementDescriptor<ProfileInfo> desc = new StatementDescriptor<>(CATEGORY, DESC_QUERY_LATEST);
-        PreparedStatement<ProfileInfo> prepared;
+        ProfileInfo info = loadLatest(vm, PROFILE_INFO_CATEGORY, PROFILE_INFO_DESC_QUERY_LATEST);
+        if (info == null) {
+            return null;
+        }
+
+        return getProfileData(info.getProfileId());
+    }
+
+    private InputStream getProfileData(String profileId) {
+        return storage.loadFile(profileId);
+    }
+
+    @Override
+    public void addStatus(ProfileStatusChange status) {
+        StatementDescriptor<ProfileStatusChange> desc = new StatementDescriptor<>(PROFILE_STATUS_CATEGORY, PROFILE_STATUS_DESC_ADD);
+        PreparedStatement<ProfileStatusChange> prepared;
+        try {
+            prepared = storage.prepareStatement(desc);
+            prepared.setString(0, status.getAgentId());
+            prepared.setString(1, status.getVmId());
+            prepared.setLong(2, status.getTimeStamp());
+            prepared.setBoolean(3, status.isStarted());
+            prepared.execute();
+        } catch (DescriptorParsingException e) {
+            logger.log(Level.SEVERE, "Preparing stmt '" + desc + "' failed!", e);
+        } catch (StatementExecutionException e) {
+            logger.log(Level.SEVERE, "Executing stmt '" + desc + "' failed!", e);
+        }
+    }
+
+    @Override
+    public ProfileStatusChange getLatestStatus(VmRef vm) {
+        return loadLatest(vm, PROFILE_STATUS_CATEGORY, PROFILE_STATUS_DESC_QUERY_LATEST);
+    }
+
+    private <T extends BasePojo> T loadLatest(VmRef vm, Category<T> category, String queryDesc) {
+        StatementDescriptor<T> desc = new StatementDescriptor<>(category, queryDesc);
+        PreparedStatement<T> prepared;
         try {
             prepared = storage.prepareStatement(desc);
             prepared.setString(0, vm.getHostRef().getAgentId());
             prepared.setString(1, vm.getVmId());
-            Cursor<ProfileInfo> cursor = prepared.executeQuery();
+            Cursor<T> cursor = prepared.executeQuery();
             if (!cursor.hasNext()) {
                 return null;
             }
-            ProfileInfo info = cursor.next();
-            return getProfileData(info.getProfileId());
+            return cursor.next();
         } catch (DescriptorParsingException e) {
             logger.log(Level.SEVERE, "Preparing stmt '" + desc + "' failed!", e);
         } catch (StatementExecutionException e) {
@@ -157,7 +214,4 @@
         return null;
     }
 
-    private InputStream getProfileData(String profileId) {
-        return storage.loadFile(profileId);
-    }
 }
--- a/vm-profiler/common/src/main/java/com/redhat/thermostat/vm/profiler/common/internal/ProfileDAOImplCategoryRegistration.java	Fri Nov 28 16:36:17 2014 -0500
+++ b/vm-profiler/common/src/main/java/com/redhat/thermostat/vm/profiler/common/internal/ProfileDAOImplCategoryRegistration.java	Fri Nov 28 16:37:02 2014 -0500
@@ -46,7 +46,8 @@
     @Override
     public Set<String> getCategoryNames() {
         Set<String> names = new HashSet<>();
-        names.add(ProfileDAOImpl.CATEGORY.getName());
+        names.add(ProfileDAOImpl.PROFILE_INFO_CATEGORY.getName());
+        names.add(ProfileDAOImpl.PROFILE_STATUS_CATEGORY.getName());
         return names;
     }
 }
--- a/vm-profiler/common/src/main/java/com/redhat/thermostat/vm/profiler/common/internal/ProfileDAOImplStatementDescriptorRegistration.java	Fri Nov 28 16:36:17 2014 -0500
+++ b/vm-profiler/common/src/main/java/com/redhat/thermostat/vm/profiler/common/internal/ProfileDAOImplStatementDescriptorRegistration.java	Fri Nov 28 16:37:02 2014 -0500
@@ -53,10 +53,12 @@
     @Override
     public Set<String> getStatementDescriptors() {
         Set<String> results = new HashSet<>();
-        results.add(ProfileDAOImpl.DESC_ADD_PROFILE_INFO);
-        results.add(ProfileDAOImpl.DESC_QUERY_BY_ID);
-        results.add(ProfileDAOImpl.DESC_QUERY_LATEST);
-        results.add(ProfileDAOImpl.DESC_INTERVAL_QUERY);
+        results.add(ProfileDAOImpl.PROFILE_INFO_DESC_ADD);
+        results.add(ProfileDAOImpl.PROFILE_INFO_DESC_QUERY_BY_ID);
+        results.add(ProfileDAOImpl.PROFILE_INFO_DESC_QUERY_LATEST);
+        results.add(ProfileDAOImpl.PROFILE_INFO_DESC_INTERVAL_QUERY);
+        results.add(ProfileDAOImpl.PROFILE_STATUS_DESC_ADD);
+        results.add(ProfileDAOImpl.PROFILE_STATUS_DESC_QUERY_LATEST);
         return results;
     }
 }
--- a/vm-profiler/common/src/test/java/com/redhat/thermostat/vm/profiler/common/internal/ProfileDAOImplCategoryRegistrationTest.java	Fri Nov 28 16:36:17 2014 -0500
+++ b/vm-profiler/common/src/test/java/com/redhat/thermostat/vm/profiler/common/internal/ProfileDAOImplCategoryRegistrationTest.java	Fri Nov 28 16:37:02 2014 -0500
@@ -48,6 +48,7 @@
     public void includesProfileInfoCategory() {
         ProfileDAOImplCategoryRegistration registration = new ProfileDAOImplCategoryRegistration();
         Set<String> names = registration.getCategoryNames();
-        assertTrue(names.contains(ProfileDAOImpl.CATEGORY.getName()));
+        assertTrue(names.contains(ProfileDAOImpl.PROFILE_INFO_CATEGORY.getName()));
+        assertTrue(names.contains(ProfileDAOImpl.PROFILE_STATUS_CATEGORY.getName()));
     }
 }
--- a/vm-profiler/common/src/test/java/com/redhat/thermostat/vm/profiler/common/internal/ProfileDAOImplStatementDescriptorRegistrationTest.java	Fri Nov 28 16:36:17 2014 -0500
+++ b/vm-profiler/common/src/test/java/com/redhat/thermostat/vm/profiler/common/internal/ProfileDAOImplStatementDescriptorRegistrationTest.java	Fri Nov 28 16:37:02 2014 -0500
@@ -48,9 +48,13 @@
     public void includesProfileInfoCategory() {
         ProfileDAOImplStatementDescriptorRegistration registration = new ProfileDAOImplStatementDescriptorRegistration();
         Set<String> names = registration.getStatementDescriptors();
-        assertTrue(names.contains(ProfileDAOImpl.DESC_ADD_PROFILE_INFO));
-        assertTrue(names.contains(ProfileDAOImpl.DESC_QUERY_LATEST));
-        assertTrue(names.contains(ProfileDAOImpl.DESC_QUERY_BY_ID));
-        assertTrue(names.contains(ProfileDAOImpl.DESC_INTERVAL_QUERY));
+
+        assertTrue(names.contains(ProfileDAOImpl.PROFILE_INFO_DESC_ADD));
+        assertTrue(names.contains(ProfileDAOImpl.PROFILE_INFO_DESC_QUERY_LATEST));
+        assertTrue(names.contains(ProfileDAOImpl.PROFILE_INFO_DESC_QUERY_BY_ID));
+        assertTrue(names.contains(ProfileDAOImpl.PROFILE_INFO_DESC_INTERVAL_QUERY));
+
+        assertTrue(names.contains(ProfileDAOImpl.PROFILE_STATUS_DESC_ADD));
+        assertTrue(names.contains(ProfileDAOImpl.PROFILE_STATUS_DESC_QUERY_LATEST));
     }
 }
--- a/vm-profiler/common/src/test/java/com/redhat/thermostat/vm/profiler/common/internal/ProfileDAOImplTest.java	Fri Nov 28 16:36:17 2014 -0500
+++ b/vm-profiler/common/src/test/java/com/redhat/thermostat/vm/profiler/common/internal/ProfileDAOImplTest.java	Fri Nov 28 16:37:02 2014 -0500
@@ -36,6 +36,27 @@
 
 package com.redhat.thermostat.vm.profiler.common.internal;
 
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import com.redhat.thermostat.storage.core.Storage;
+
 public class ProfileDAOImplTest {
 
+    private Storage storage;
+
+    @Before
+    public void setUp() {
+        storage = mock(Storage.class);
+    }
+    @Test
+    public void registersCategories() throws Exception {
+        new ProfileDAOImpl(storage);
+
+        verify(storage).registerCategory(ProfileDAOImpl.PROFILE_INFO_CATEGORY);
+        verify(storage).registerCategory(ProfileDAOImpl.PROFILE_STATUS_CATEGORY);
+    }
 }