1 /* gbp-sysprof-workspace-addin.c
2  *
3  * Copyright 2016-2019 Christian Hergert <chergert@redhat.com>
4  *
5  * This program is free software: you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation, either version 3 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
17  *
18  * SPDX-License-Identifier: GPL-3.0-or-later
19  */
20 
21 #define G_LOG_DOMAIN "gbp-sysprof-workspace-addin"
22 
23 #include "config.h"
24 
25 #include <dazzle.h>
26 #include <glib/gi18n.h>
27 #include <sysprof-ui.h>
28 
29 #include "gbp-sysprof-surface.h"
30 #include "gbp-sysprof-workspace-addin.h"
31 
32 struct _GbpSysprofWorkspaceAddin
33 {
34   GObject                parent_instance;
35 
36   GSimpleActionGroup    *actions;
37 
38   GbpSysprofSurface     *surface;
39   IdeWorkspace          *workspace;
40 };
41 
42 static void workspace_addin_iface_init (IdeWorkspaceAddinInterface *iface);
43 
G_DEFINE_FINAL_TYPE_WITH_CODE(GbpSysprofWorkspaceAddin,gbp_sysprof_workspace_addin,G_TYPE_OBJECT,G_IMPLEMENT_INTERFACE (IDE_TYPE_WORKSPACE_ADDIN,workspace_addin_iface_init))44 G_DEFINE_FINAL_TYPE_WITH_CODE (GbpSysprofWorkspaceAddin, gbp_sysprof_workspace_addin, G_TYPE_OBJECT,
45                          G_IMPLEMENT_INTERFACE (IDE_TYPE_WORKSPACE_ADDIN, workspace_addin_iface_init))
46 
47 static void
48 profiler_child_spawned (IdeRunner       *runner,
49                         const gchar     *identifier,
50                         SysprofProfiler *profiler)
51 {
52 #ifdef G_OS_UNIX
53   GPid pid = 0;
54 
55   g_assert (IDE_IS_MAIN_THREAD ());
56   g_assert (SYSPROF_IS_PROFILER (profiler));
57   g_assert (identifier != NULL);
58   g_assert (IDE_IS_RUNNER (runner));
59 
60   pid = g_ascii_strtoll (identifier, NULL, 10);
61 
62   if (pid == 0)
63     {
64       g_warning ("Failed to parse integer value from %s", identifier);
65       return;
66     }
67 
68   IDE_TRACE_MSG ("Adding pid %s to profiler", identifier);
69 
70   sysprof_profiler_add_pid (profiler, pid);
71   sysprof_profiler_start (profiler);
72 #endif
73 }
74 
75 static void
runner_exited_cb(IdeRunner * runner,SysprofProfiler * profiler)76 runner_exited_cb (IdeRunner       *runner,
77                   SysprofProfiler *profiler)
78 {
79   g_assert (IDE_IS_MAIN_THREAD ());
80   g_assert (IDE_IS_RUNNER (runner));
81   g_assert (SYSPROF_IS_PROFILER (profiler));
82 
83   if (sysprof_profiler_get_is_running (profiler))
84     sysprof_profiler_stop (profiler);
85 }
86 
87 static void
foreach_fd(gint dest_fd,gint fd,gpointer user_data)88 foreach_fd (gint     dest_fd,
89             gint     fd,
90             gpointer user_data)
91 {
92   IdeRunner *runner = user_data;
93 
94   g_assert (IDE_IS_RUNNER (runner));
95   g_assert (dest_fd >= 0);
96   g_assert (fd >= 0);
97 
98   ide_runner_take_fd (runner, dup (fd), dest_fd);
99 }
100 
101 static void
profiler_run_handler(IdeRunManager * run_manager,IdeRunner * runner,gpointer user_data)102 profiler_run_handler (IdeRunManager *run_manager,
103                       IdeRunner     *runner,
104                       gpointer       user_data)
105 {
106   GbpSysprofWorkspaceAddin *self = user_data;
107   g_autoptr(SysprofProfiler) profiler = NULL;
108   g_autoptr(SysprofSource) app_source = NULL;
109   g_autoptr(SysprofSpawnable) spawnable = NULL;
110   g_autoptr(GPtrArray) sources = NULL;
111   g_auto(GStrv) argv = NULL;
112   const gchar * const *env;
113   IdeEnvironment *ienv;
114 
115   g_assert (IDE_IS_MAIN_THREAD ());
116   g_assert (GBP_IS_SYSPROF_WORKSPACE_ADDIN (self));
117   g_assert (IDE_IS_RUNNER (runner));
118   g_assert (IDE_IS_RUN_MANAGER (run_manager));
119 
120   sources = g_ptr_array_new_with_free_func (g_object_unref);
121 
122   profiler = sysprof_local_profiler_new ();
123 
124   /*
125    * Currently we require whole-system because otherwise we can get a situation
126    * where we only watch the spawning process (say jhbuild, flatpak, etc).
127    * Longer term we either need a way to follow-children and/or limit to a
128    * cgroup/process-group.
129    */
130   sysprof_profiler_set_whole_system (profiler, TRUE);
131 
132 #ifdef __linux__
133   {
134     g_ptr_array_add (sources, sysprof_proc_source_new ());
135     g_ptr_array_add (sources, sysprof_perf_source_new ());
136     g_ptr_array_add (sources, sysprof_hostinfo_source_new ());
137     g_ptr_array_add (sources, sysprof_memory_source_new ());
138     g_ptr_array_add (sources, sysprof_proxy_source_new (G_BUS_TYPE_SYSTEM,
139                                                         "org.gnome.Sysprof3",
140                                                         "/org/gnome/Sysprof3/RAPL"));
141     g_ptr_array_add (sources, sysprof_netdev_source_new ());
142   }
143 #endif
144 
145   g_ptr_array_add (sources, sysprof_gjs_source_new ());
146   g_ptr_array_add (sources, sysprof_symbols_source_new ());
147 
148   /* Allow the app to submit us data if it supports "SYSPROF_TRACE_FD" */
149   app_source = sysprof_tracefd_source_new ();
150   sysprof_tracefd_source_set_envvar (SYSPROF_TRACEFD_SOURCE (app_source), "SYSPROF_TRACE_FD");
151   g_ptr_array_add (sources, g_object_ref (app_source));
152 
153   /*
154    * TODO:
155    *
156    * We need to synchronize the inferior with the parent here. Ideally, we would
157    * prepend the application launch (to some degree) with the application we want
158    * to execute. In this case, we might want to add a "gnome-builder-sysprof"
159    * helper that will synchronize with the parent, and then block until we start
160    * the process (with the appropriate pid) before exec() otherwise we could
161    * miss the exit of the app and race to add the pid to the profiler.
162    */
163 
164   g_signal_connect_object (runner,
165                            "spawned",
166                            G_CALLBACK (profiler_child_spawned),
167                            profiler,
168                            0);
169 
170   g_signal_connect_object (runner,
171                            "exited",
172                            G_CALLBACK (runner_exited_cb),
173                            profiler,
174                            0);
175 
176   /*
177    * We need to allow the sources to modify the execution environment, so copy
178    * the environment into the spawnable, modify it, and the propagate back.
179    */
180   argv = ide_runner_get_argv (runner);
181   ienv = ide_runner_get_environment (runner);
182 
183   spawnable = sysprof_spawnable_new ();
184   sysprof_spawnable_append_args (spawnable, (const gchar * const *)argv);
185   sysprof_spawnable_set_starting_fd (spawnable, ide_runner_get_max_fd (runner) + 1);
186 
187   for (guint i = 0; i < sources->len; i++)
188     {
189       SysprofSource *source = g_ptr_array_index (sources, i);
190 
191       if (source != NULL)
192         {
193           sysprof_profiler_add_source (profiler, source);
194           sysprof_source_modify_spawn (source, spawnable);
195         }
196     }
197 
198   /* TODO: Propagate argv back to runner.
199    *
200    * Currently this is a non-issue because none of our sources modify argv.
201    * So doing it now is just brittle for no benefit.
202    */
203 
204   if ((env = sysprof_spawnable_get_environ (spawnable)))
205     {
206       for (guint i = 0; env[i] != NULL; i++)
207         {
208           g_autofree gchar *key = NULL;
209           g_autofree gchar *value = NULL;
210 
211           if (ide_environ_parse (env[i], &key, &value))
212             ide_environment_setenv (ienv, key, value);
213         }
214     }
215 
216   sysprof_spawnable_foreach_fd (spawnable, foreach_fd, runner);
217 
218   gbp_sysprof_surface_add_profiler (self->surface, profiler);
219 
220   ide_workspace_set_visible_surface (self->workspace, IDE_SURFACE (self->surface));
221 }
222 
223 static void
gbp_sysprof_workspace_addin_open(GbpSysprofWorkspaceAddin * self,GFile * file)224 gbp_sysprof_workspace_addin_open (GbpSysprofWorkspaceAddin *self,
225                                   GFile                    *file)
226 {
227   g_assert (GBP_IS_SYSPROF_WORKSPACE_ADDIN (self));
228   g_assert (G_IS_FILE (file));
229 
230   if (!g_file_is_native (file))
231     g_warning ("Can only open local sysprof capture files.");
232   else
233     gbp_sysprof_surface_open (self->surface, file);
234 }
235 
236 static void
open_profile_action(GSimpleAction * action,GVariant * variant,gpointer user_data)237 open_profile_action (GSimpleAction *action,
238                      GVariant      *variant,
239                      gpointer       user_data)
240 {
241   GbpSysprofWorkspaceAddin *self = user_data;
242   g_autoptr(GFile) workdir = NULL;
243   GtkFileChooserNative *native;
244   GtkFileFilter *filter;
245   IdeContext *context;
246   gint ret;
247 
248   g_assert (IDE_IS_MAIN_THREAD ());
249   g_assert (GBP_IS_SYSPROF_WORKSPACE_ADDIN (self));
250   g_assert (IDE_IS_WORKSPACE (self->workspace));
251   g_assert (GBP_IS_SYSPROF_SURFACE (self->surface));
252 
253   ide_workspace_set_visible_surface (self->workspace, IDE_SURFACE (self->surface));
254 
255   context = ide_workspace_get_context (self->workspace);
256   workdir = ide_context_ref_workdir (context);
257 
258   native = gtk_file_chooser_native_new (_("Open Sysprof Capture…"),
259                                         GTK_WINDOW (self->workspace),
260                                         GTK_FILE_CHOOSER_ACTION_OPEN,
261                                         _("Open"),
262                                         _("Cancel"));
263   gtk_file_chooser_set_current_folder_file (GTK_FILE_CHOOSER (native), workdir, NULL);
264 
265   /* Add our filter for sysprof capture files.  */
266   filter = gtk_file_filter_new ();
267   gtk_file_filter_set_name (filter, _("Sysprof Capture (*.syscap)"));
268   gtk_file_filter_add_pattern (filter, "*.syscap");
269   gtk_file_chooser_add_filter (GTK_FILE_CHOOSER (native), filter);
270 
271   /* And all files now */
272   filter = gtk_file_filter_new ();
273   gtk_file_filter_set_name (filter, _("All Files"));
274   gtk_file_filter_add_pattern (filter, "*");
275   gtk_file_chooser_add_filter (GTK_FILE_CHOOSER (native), filter);
276 
277   /* Unlike gtk_dialog_run(), this will handle processing
278    * various I/O events and so should be safe to use.
279    */
280   ret = gtk_native_dialog_run (GTK_NATIVE_DIALOG (native));
281 
282   if (ret == GTK_RESPONSE_ACCEPT)
283     {
284       g_autoptr(GFile) file = NULL;
285 
286       file = gtk_file_chooser_get_file (GTK_FILE_CHOOSER (native));
287       if (G_IS_FILE (file))
288         gbp_sysprof_workspace_addin_open (self, file);
289     }
290 
291   gtk_native_dialog_hide (GTK_NATIVE_DIALOG (native));
292   gtk_native_dialog_destroy (GTK_NATIVE_DIALOG (native));
293 }
294 
295 static void
run_cb(GSimpleAction * action,GVariant * param,gpointer user_data)296 run_cb (GSimpleAction *action,
297         GVariant      *param,
298         gpointer       user_data)
299 {
300   GbpSysprofWorkspaceAddin *self = user_data;
301 
302   g_assert (GBP_IS_SYSPROF_WORKSPACE_ADDIN (self));
303 
304   if (self->workspace != NULL)
305     dzl_gtk_widget_action (GTK_WIDGET (self->workspace),
306                            "run-manager",
307                            "run-with-handler",
308                            g_variant_new_string ("profiler"));
309 }
310 
311 static void
show_cb(GSimpleAction * action,GVariant * param,gpointer user_data)312 show_cb (GSimpleAction *action,
313          GVariant      *param,
314          gpointer       user_data)
315 {
316   GbpSysprofWorkspaceAddin *self = user_data;
317 
318   g_assert (GBP_IS_SYSPROF_WORKSPACE_ADDIN (self));
319 
320   if (self->workspace != NULL)
321     ide_workspace_set_visible_surface (self->workspace, IDE_SURFACE (self->surface));
322 }
323 
324 static void
gbp_sysprof_workspace_addin_finalize(GObject * object)325 gbp_sysprof_workspace_addin_finalize (GObject *object)
326 {
327   GbpSysprofWorkspaceAddin *self = (GbpSysprofWorkspaceAddin *)object;
328 
329   g_assert (IDE_IS_MAIN_THREAD ());
330 
331   g_clear_object (&self->actions);
332 
333   G_OBJECT_CLASS (gbp_sysprof_workspace_addin_parent_class)->finalize (object);
334 }
335 
336 static void
gbp_sysprof_workspace_addin_class_init(GbpSysprofWorkspaceAddinClass * klass)337 gbp_sysprof_workspace_addin_class_init (GbpSysprofWorkspaceAddinClass *klass)
338 {
339   GObjectClass *object_class = G_OBJECT_CLASS (klass);
340 
341   object_class->finalize = gbp_sysprof_workspace_addin_finalize;
342 }
343 
344 static void
gbp_sysprof_workspace_addin_init(GbpSysprofWorkspaceAddin * self)345 gbp_sysprof_workspace_addin_init (GbpSysprofWorkspaceAddin *self)
346 {
347   static const GActionEntry entries[] = {
348     { "open-profile", open_profile_action },
349     { "run", run_cb },
350     { "show", show_cb },
351   };
352 
353   g_assert (IDE_IS_MAIN_THREAD ());
354 
355   self->actions = g_simple_action_group_new ();
356 
357   g_action_map_add_action_entries (G_ACTION_MAP (self->actions),
358                                    entries,
359                                    G_N_ELEMENTS (entries),
360                                    self);
361 }
362 
363 static void
gbp_sysprof_workspace_addin_check_supported_cb(GObject * object,GAsyncResult * result,gpointer user_data)364 gbp_sysprof_workspace_addin_check_supported_cb (GObject      *object,
365                                                 GAsyncResult *result,
366                                                 gpointer      user_data)
367 {
368   g_autoptr(GbpSysprofWorkspaceAddin) self = user_data;
369   g_autoptr(GError) error = NULL;
370   IdeRunManager *run_manager;
371   IdeContext *context;
372 
373   g_assert (GBP_IS_SYSPROF_WORKSPACE_ADDIN (self));
374   g_assert (G_IS_ASYNC_RESULT (result));
375 
376   /* Check if we're unloaded */
377   if (self->workspace == NULL)
378     return;
379 
380   if (!sysprof_check_supported_finish (result, &error))
381     {
382       g_warning ("Sysprof-3 is not supported, will not enable profiler: %s",
383                  error->message);
384       return;
385     }
386 
387   gtk_widget_insert_action_group (GTK_WIDGET (self->workspace),
388                                   "profiler",
389                                   G_ACTION_GROUP (self->actions));
390 
391   /* Register our custom run handler to activate the profiler. */
392   context = ide_workspace_get_context (self->workspace);
393   run_manager = ide_run_manager_from_context (context);
394   ide_run_manager_add_handler (run_manager,
395                                "profiler",
396                                _("Run with Profiler"),
397                                "builder-profiler-symbolic",
398                                "<primary>F8",
399                                profiler_run_handler,
400                                self,
401                                NULL);
402 
403   /* Add the surface to the workspace. */
404   self->surface = g_object_new (GBP_TYPE_SYSPROF_SURFACE,
405                                 "visible", TRUE,
406                                 NULL);
407   g_signal_connect (self->surface,
408                     "destroy",
409                     G_CALLBACK (gtk_widget_destroyed),
410                     &self->surface);
411   ide_workspace_add_surface (self->workspace, IDE_SURFACE (self->surface));
412 }
413 
414 static void
gbp_sysprof_workspace_addin_load(IdeWorkspaceAddin * addin,IdeWorkspace * workspace)415 gbp_sysprof_workspace_addin_load (IdeWorkspaceAddin *addin,
416                                   IdeWorkspace      *workspace)
417 {
418   GbpSysprofWorkspaceAddin *self = (GbpSysprofWorkspaceAddin *)addin;
419 
420   g_assert (IDE_IS_MAIN_THREAD ());
421   g_assert (GBP_IS_SYSPROF_WORKSPACE_ADDIN (self));
422   g_assert (IDE_IS_PRIMARY_WORKSPACE (workspace));
423 
424   self->workspace = workspace;
425 
426   sysprof_check_supported_async (NULL,
427                                  gbp_sysprof_workspace_addin_check_supported_cb,
428                                  g_object_ref (self));
429 }
430 
431 static void
gbp_sysprof_workspace_addin_unload(IdeWorkspaceAddin * addin,IdeWorkspace * workspace)432 gbp_sysprof_workspace_addin_unload (IdeWorkspaceAddin *addin,
433                                     IdeWorkspace      *workspace)
434 {
435   GbpSysprofWorkspaceAddin *self = (GbpSysprofWorkspaceAddin *)addin;
436   IdeRunManager *run_manager;
437   IdeContext *context;
438 
439   g_assert (GBP_IS_SYSPROF_WORKSPACE_ADDIN (self));
440   g_assert (IDE_IS_WORKSPACE (workspace));
441 
442   context = ide_workspace_get_context (workspace);
443 
444   gtk_widget_insert_action_group (GTK_WIDGET (workspace), "profiler", NULL);
445 
446   run_manager = ide_run_manager_from_context (context);
447   ide_run_manager_remove_handler (run_manager, "profiler");
448 
449   if (self->surface != NULL)
450     gtk_widget_destroy (GTK_WIDGET (self->surface));
451 
452   self->surface = NULL;
453   self->workspace = NULL;
454 }
455 
456 static void
workspace_addin_iface_init(IdeWorkspaceAddinInterface * iface)457 workspace_addin_iface_init (IdeWorkspaceAddinInterface *iface)
458 {
459   iface->load = gbp_sysprof_workspace_addin_load;
460   iface->unload = gbp_sysprof_workspace_addin_unload;
461 }
462