public inbox for cygwin-patches@cygwin.com
 help / color / mirror / Atom feed
* [PATCH 1/3] Cygwin: New tool: profiler
@ 2021-07-16  4:49 Mark Geisert
  2021-07-16  4:49 ` [PATCH 2/3] Cygwin: New tool: gmondump Mark Geisert
                   ` (2 more replies)
  0 siblings, 3 replies; 9+ messages in thread
From: Mark Geisert @ 2021-07-16  4:49 UTC (permalink / raw)
  To: cygwin-patches

The new tool formerly known as cygmon is renamed to 'profiler'.  For the
name I considered 'ipsampler' and could not think of any others.  I'm open
to a different name if any is suggested.

I decided that a discussion of the pros and cons of this profiler vs the
existing ssp should probably be in the "Profiling Cygwin Programs" section
of the Cygwin User's Guide rather than in the help for either.  That
material will be supplied at some point.

CONTEXT buffers are made child-specific and thus thread-specific since
there is one profiler thread for each child program being profiled.

The SetThreadPriority() warning comment has been expanded.

chmod() works on Cygwin so the "//XXX ineffective" comment is gone.

I decided to make the "sample all executable sections" and "sample
dynamically generated code" suggestions simply expanded comments for now.

The profiler program is now a Cygwin exe rather than a native exe.

---
 winsup/utils/profiler.cc | 1109 ++++++++++++++++++++++++++++++++++++++
 1 file changed, 1109 insertions(+)
 create mode 100644 winsup/utils/profiler.cc

diff --git a/winsup/utils/profiler.cc b/winsup/utils/profiler.cc
new file mode 100644
index 000000000..d1a01c3a2
--- /dev/null
+++ b/winsup/utils/profiler.cc
@@ -0,0 +1,1109 @@
+/*
+    profiler.cc
+    Periodically samples IP of a process and its DLLs; writes gprof data files.
+
+    Written by Mark Geisert <mark@maxrnd.com>, who admits to
+    copying pretty liberally from strace.cc.  h/t to cgf for strace!
+
+    This file is part of Cygwin.
+
+    This software is a copyrighted work licensed under the terms of the
+    Cygwin license.  Please consult the file "CYGWIN_LICENSE" for details.
+*/
+
+#define WIN32_LEAN_AND_MEAN
+#include <winternl.h>
+
+#define cygwin_internal cygwin_internal_dontuse
+#include <errno.h>
+#include <fcntl.h>
+#include <getopt.h>
+#include <io.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+#include <unistd.h>
+#include <sys/cygwin.h>
+#include "cygwin/version.h"
+#include "cygtls_padsize.h"
+#include "gcc_seh.h"
+typedef unsigned short ushort;
+typedef uint16_t u_int16_t; // Non-standard sized type needed by ancient gmon.h
+#define NO_GLOBALS_H
+#include "gmon.h"
+#include "path.h"
+#undef cygwin_internal
+
+/* Undo this #define from winsup.h. */
+#ifdef ExitThread
+#undef ExitThread
+#endif
+
+#define SCALE_SHIFT 2 // == 4 bytes of address space per bucket
+#define MS_VC_EXCEPTION 0x406D1388 // thread name notification from child
+
+DWORD       child_pid;
+int         debugging = 0;
+void       *drive_map;
+int         events = 0;
+int         forkprofile = 0;
+int         new_window;
+int         numprocesses;
+FILE       *ofile = stdout;
+const char *pgm;
+char       *prefix = (char *) "gmon.out";
+int         samplerate = 100; // in Hz; up to 1000 might work
+int         verbose = 0;
+
+void __attribute__ ((__noreturn__))
+usage (FILE *where = stderr)
+{
+  fprintf (where, "\
+Usage: %s [OPTIONS] <command-line>\n\
+   or: %s [OPTIONS] -p <pid>\n\
+\n\
+Profiles a command or process by sampling its IP (instruction pointer).\n\
+OPTIONS are:\n\
+\n\
+  -d, --debug            Display debugging messages (toggle: default false)\n\
+  -e, --events           Display Windows DEBUG_EVENTS (toggle: default false)\n\
+  -f, --fork-profile     Profile child processes (toggle: default false)\n\
+  -h, --help             Display usage information and exit\n\
+  -o, --output=FILENAME  Write output to file FILENAME rather than stdout\n\
+  -p, --pid=N            Attach to running program with Cygwin pid N\n\
+                         ...                    or with Windows pid -N\n\
+  -s, --sample-rate=N    Set IP sampling rate to N Hz (default 100)\n\
+  -v, --verbose          Display more status messages (toggle: default false)\n\
+  -V, --version          Display version information and exit\n\
+  -w, --new-window       Launch given command in a new window\n\
+\n", pgm, pgm);
+
+  exit (where == stderr ? 1 : 0 );
+}
+
+/* A span is a memory address range covering an EXE's or DLL's .text segment. */
+struct span_list
+{
+  WCHAR  *name;
+  LPVOID  base;
+  size_t  textlo;
+  size_t  texthi;
+  int     hitcount;
+  int     hitbuckets;
+  int     numbuckets;
+  int    *buckets;
+  struct span_list *next;
+};
+
+/* A thread. */
+struct thread_list
+{
+  DWORD   tid;
+  HANDLE  hthread;
+  WCHAR  *name;
+  struct thread_list *next;
+};
+
+/* A child is any process being sampled in this profiler run. */
+struct child_list
+{
+  DWORD  pid;
+  volatile int profiling;
+  HANDLE hproc;
+  HANDLE hquitevt;
+  HANDLE hprofthr;
+  CONTEXT            *context;
+  struct thread_list *threads;
+  struct span_list   *spans;
+  struct child_list  *next;
+};
+
+child_list children;
+typedef struct child_list child;
+
+void
+note (const char *fmt, ...)
+{
+  va_list args;
+  char    buf[4096];
+
+  va_start (args, fmt);
+  vsprintf (buf, fmt, args);
+  va_end (args);
+
+  fputs (buf, ofile);
+  fflush (ofile);
+}
+
+void
+warn (int geterrno, const char *fmt, ...)
+{
+  va_list args;
+  char    buf[4096];
+
+  va_start (args, fmt);
+  sprintf (buf, "%s: ", pgm);
+  vsprintf (strchr (buf, '\0'), fmt, args);
+  va_end (args);
+  if (geterrno)
+    perror (buf);
+  else
+    {
+      fputs (buf, ofile);
+      fputs ("\n", ofile);
+      fflush (ofile);
+    }
+}
+
+void __attribute__ ((noreturn))
+error (int geterrno, const char *fmt, ...)
+{
+  va_list args;
+
+  va_start (args, fmt);
+  warn (geterrno, fmt, args);
+  va_end (args);
+
+  exit (1);
+}
+
+size_t
+sample (CONTEXT *context, HANDLE h)
+{
+  size_t status;
+
+  if (-1U == SuspendThread (h))
+    return 0ULL;
+  status = GetThreadContext (h, context);
+  if (-1U == ResumeThread (h))
+    if (verbose)
+      note ("*** unable to resume thread %d; continuing anyway\n", h);
+
+  if (0 == status)
+    {
+      if (verbose)
+        note ("*** unable to get context for thread %d\n", h);
+      return 0ULL;
+    }
+  else
+//TODO this approach does not support 32-bit executables on 64-bit
+#ifdef __x86_64__
+    return context->Rip;
+#else
+    return context->Eip;
+#endif
+}
+
+void
+bump_bucket (child *c, size_t pc)
+{
+  span_list *s = c->spans;
+
+//note ("%lu %p\n", c->pid, pc);
+  if (pc == 0ULL)
+    return;
+  while (s)
+    {
+      if (pc >= s->textlo && pc < s->texthi)
+        {
+          if (0 == s->buckets[(pc - s->textlo) >> SCALE_SHIFT]++)
+            ++s->hitbuckets;
+          ++s->hitcount;
+          return;
+        }
+      s = s->next;
+    }
+
+  /*TODO If the child has dynamically created an executable memory region, we
+   *     won't notice it until the profiler thread happens to sample an
+   *     instruction in that region.  We could then add a new span to record
+   *     hits on this new region. (QueryVirtualMemory to obtain limits?)
+   *
+   *     Note that if the app dynamically adds and deletes such regions, the
+   *     profiling info on them will be confusing if their addresses overlap.
+   */
+  if (verbose)
+    note ("*** pc %p out of range for pid %lu\n", pc, c->pid);
+}
+
+/* profiler runs on its own thread; each child has a separate profiler. */
+DWORD WINAPI
+profiler (void *vp)
+{
+  child *c = (child *) vp;
+
+  while (c->profiling)
+    {
+      thread_list *t = c->threads;
+
+      while (t)
+        {
+          if (t->hthread)
+            bump_bucket (c, sample (c->context, t->hthread));
+          t = t->next;
+        }
+
+      if (WaitForSingleObject (c->hquitevt, 1000 / samplerate) == WAIT_OBJECT_0)
+        break;
+    }
+
+  return 0;
+}
+
+void
+start_profiler (child *c)
+{
+  DWORD  tid;
+
+  if (verbose)
+    note ("*** start profiler thread on pid %lu\n", c->pid);
+  c->context = (CONTEXT *) calloc (1, sizeof (CONTEXT));
+  if (!c->context)
+    error (0, "unable to allocate CONTEXT buffer\n");
+  c->context->ContextFlags = CONTEXT_CONTROL;
+  c->hquitevt = CreateEvent (NULL, TRUE, FALSE, NULL);
+  if (!c->hquitevt)
+    error (0, "unable to create quit event\n");
+  c->profiling = 1;
+  c->hprofthr = CreateThread (NULL, 0, profiler, (void *) c, 0, &tid);
+  if (!c->hprofthr)
+    error (0, "unable to create profiling thread\n");
+
+  /* There is a temptation to raise the execution priority of the profiling
+   * threads.  Don't do this, or at least don't do it this way.  Testing
+   * showed that it was possible to starve system processes which makes the
+   * system unresponsive.  Raising prio doesn't seem to be needed at all.
+   *
+  SetThreadPriority (c->hprofthr, THREAD_PRIORITY_TIME_CRITICAL);
+   */
+}
+
+void
+stop_profiler (child *c)
+{
+  if (verbose)
+    note ("*** stop profiler thread on pid %lu\n", c->pid);
+  c->profiling = 0;
+  SignalObjectAndWait (c->hquitevt, c->hprofthr, INFINITE, FALSE);
+  CloseHandle (c->hquitevt);
+  CloseHandle (c->hprofthr);
+  c->hquitevt = c->hprofthr = 0;
+}
+
+/* Create a gmon.out file for each EXE or DLL that has at least one sample. */
+void
+dump_profile_data (child *c)
+{
+  int        fd;
+  char       filename[MAX_PATH + 1];
+  struct gmonhdr hdr;
+  span_list *s = c->spans;
+
+  while (s)
+    {
+      if (s->hitbuckets == 0)
+        {
+          s = s->next;
+          continue;
+        }
+
+      if (s->name)
+        {
+          WCHAR *name = 1 + wcsrchr (s->name, L'\\');
+          sprintf (filename, "%s.%u.%ls", prefix, c->pid, name);
+        }
+      else
+        sprintf (filename, "%s.%u", prefix, c->pid);
+
+      fd = open (filename, O_CREAT | O_TRUNC | O_WRONLY | O_BINARY);
+      if (fd < 0)
+        error (0, "dump_profile_data: unable to create %s\n", filename);
+
+      memset (&hdr, 0, sizeof (hdr));
+      hdr.lpc = s->textlo;
+      hdr.hpc = s->texthi;
+      hdr.ncnt = s->numbuckets * sizeof (short) + sizeof (hdr);
+      hdr.version = GMONVERSION;
+      hdr.profrate = samplerate;
+
+      /* Our buckets hold more than gmon standard buckets, so truncate here. */
+      ushort *gmonbuckets = (ushort *) calloc (s->numbuckets, sizeof (ushort));
+      for (int i = 0; i < s->numbuckets; i++)
+        {
+          if (s->buckets[i])
+            {
+              if (s->buckets[i] > 65535)
+                {
+                  note ("  WARNING: bucket %d: value %d truncated to %d\n",
+                        i, s->buckets[i], 65535);
+                  gmonbuckets[i] = 65535;
+                }
+              else
+                gmonbuckets[i] = s->buckets[i];
+            }
+        }
+
+      write (fd, &hdr, sizeof (hdr));
+      write (fd, gmonbuckets, hdr.ncnt - sizeof (hdr));
+      note ("%d %s across %d %s written to %s\n", s->hitcount,
+            s->hitcount == 1 ? "sample" : "samples", s->hitbuckets,
+            s->hitbuckets == 1 ? "bucket" : "buckets", filename);
+      close (fd);
+      chmod (filename, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
+      free (gmonbuckets);
+
+      s = s->next;
+    }
+}
+
+HANDLE lasth;
+DWORD  lastpid = 0;
+
+child *
+get_child (DWORD pid)
+{
+  child *c;
+
+  for (c = &children; (c = c->next) != NULL;)
+    if (c->pid == pid)
+      return (child *) c;
+
+  return NULL;
+}
+
+void add_span (DWORD, WCHAR *, LPVOID, HANDLE);
+
+void
+add_child (DWORD pid, WCHAR *name, LPVOID base, HANDLE hproc)
+{
+  if (!get_child (pid))
+    {
+      child *c = children.next;
+      children.next = (child *) calloc (1, sizeof (child));
+      children.next->next = c;
+      lastpid = children.next->pid = pid;
+      lasth = children.next->hproc = hproc;
+      add_span (pid, name, base, hproc);
+      start_profiler (children.next);
+      numprocesses++;
+      if (verbose)
+        note ("*** Windows process %lu attached\n", pid);
+    }
+}
+
+void
+remove_child (DWORD pid)
+{
+  child *c;
+
+  if (pid == lastpid)
+    lastpid = 0;
+  for (c = &children; c->next != NULL; c = c->next)
+    if (c->next->pid == pid)
+      {
+        child *c1 = c->next;
+        c->next = c1->next;
+        stop_profiler (c1);
+        dump_profile_data (c1);
+        CloseHandle (c1->hproc);
+        c1->hproc = 0;
+        free (c1);
+        if (verbose)
+          note ("*** Windows process %lu detached\n", pid);
+        numprocesses--;
+        return;
+      }
+
+  error (0, "no process id %d found", pid);
+}
+
+void
+add_thread (DWORD pid, DWORD tid, HANDLE h, WCHAR *name)
+{
+  child *c = get_child (pid);
+
+  if (!c)
+    error (0, "add_thread: pid %lu not found\n", pid);
+
+  thread_list *t = (thread_list *) calloc (1, sizeof (thread_list));
+  t->tid = tid;
+  t->hthread = h;
+  t->name = name;
+
+  t->next = c->threads;
+  c->threads = t;
+}
+
+void
+remove_thread (DWORD pid, DWORD tid)
+{
+  child *c = get_child (pid);
+
+  if (!c)
+    error (0, "remove_thread: pid %lu not found\n", pid);
+
+  thread_list *t = c->threads;
+  while (t)
+    {
+      if (t->tid == tid)
+        {
+          /*TODO We don't free(t), we just zero it out.  Maybe revisit this. */
+          t->tid = 0;
+          CloseHandle (t->hthread);
+          t->hthread = 0;
+          if (t->name)
+            free (t->name);
+          t->name = NULL;
+          return;
+        }
+      t = t->next;
+    }
+
+  error (0, "remove_thread: pid %lu tid %lu not found\n", pid, tid);
+}
+
+void
+read_child (void *buf, SIZE_T size, void *addr, HANDLE h)
+{
+  SIZE_T len;
+
+  if (debugging)
+    note ("read %d bytes at %p from handle %d\n", size, addr, h);
+  if (0 == ReadProcessMemory (h, addr, buf, size, &len))
+    error (0, "read_child: failed\n");
+  if (len != size)
+    error (0, "read_child: asked for %d bytes but got %d\n", size, len);
+}
+
+IMAGE_SECTION_HEADER *
+find_text_section (LPVOID base, HANDLE h)
+{
+  static IMAGE_SECTION_HEADER asect;
+  DWORD  lfanew;
+  WORD   machine;
+  WORD   nsects;
+  DWORD  ntsig;
+  char  *ptr = (char *) base;
+
+  IMAGE_DOS_HEADER *idh = (IMAGE_DOS_HEADER *) ptr;
+  read_child ((void *) &lfanew, sizeof (lfanew), &idh->e_lfanew, h);
+  ptr += lfanew;
+
+  /* Code handles 32- or 64-bit headers depending on compilation environment. */
+  /*TODO It doesn't yet handle 32-bit headers on 64-bit Cygwin or v/v.        */
+  IMAGE_NT_HEADERS *inth = (IMAGE_NT_HEADERS *) ptr;
+  read_child ((void *) &ntsig, sizeof (ntsig), &inth->Signature, h);
+  if (ntsig != IMAGE_NT_SIGNATURE)
+    error (0, "find_text_section: NT signature not found\n");
+
+  read_child ((void *) &machine, sizeof (machine),
+              &inth->FileHeader.Machine, h);
+#ifdef __x86_64__
+  if (machine != IMAGE_FILE_MACHINE_AMD64)
+#else
+  if (machine != IMAGE_FILE_MACHINE_I386)
+#endif
+    error (0, "target program was built for different machine architecture\n");
+
+  read_child ((void *) &nsects, sizeof (nsects),
+              &inth->FileHeader.NumberOfSections, h);
+  ptr += sizeof (*inth);
+
+  IMAGE_SECTION_HEADER *ish = (IMAGE_SECTION_HEADER *) ptr;
+  for (int i = 0; i < nsects; i++)
+    {
+      read_child ((void *) &asect, sizeof (asect), ish, h);
+      if (0 == memcmp (".text\0\0\0", &asect.Name, 8))
+        return &asect;
+      ish++;
+    }
+
+  error (0, ".text section not found\n");
+}
+
+//TODO Extend add_span to add all executable sections of this exe/dll
+void
+add_span (DWORD pid, WCHAR *name, LPVOID base, HANDLE h)
+{
+  child *c = get_child (pid);
+
+  if (!c)
+    error (0, "add_span: pid %lu not found\n", pid);
+
+  IMAGE_SECTION_HEADER *sect = find_text_section (base, c->hproc);
+  span_list *s = (span_list *) calloc (1, sizeof (span_list));
+  s->name = name;
+  s->base = base;
+  s->textlo = sect->VirtualAddress + (size_t) base;
+  s->texthi = s->textlo + sect->Misc.VirtualSize;
+  s->numbuckets = (s->texthi - s->textlo) >> SCALE_SHIFT;
+  s->buckets = (int *) calloc (s->numbuckets, sizeof (int));
+  if (debugging)
+    note ("    span %p - %p, size %X, numbuckets %d\n",
+          s->textlo, s->texthi, s->texthi - s->textlo, s->numbuckets);
+
+  s->next = c->spans;
+  c->spans = s;
+}
+
+#define LINE_BUF_CHUNK 128
+
+class linebuf
+{
+  size_t  alloc;
+public:
+  size_t  ix;
+  char   *buf;
+  linebuf ()
+  {
+    ix = 0;
+    alloc = 0;
+    buf = NULL;
+  }
+ ~linebuf ()
+  {
+    if (buf)
+      free (buf);
+  }
+  void add (const char *what, int len);
+  void add (const char *what)
+  {
+    add (what, strlen (what));
+  }
+  void prepend (const char *what, int len);
+};
+
+void
+linebuf::add (const char *what, int len)
+{
+  size_t newix;
+
+  if ((newix = ix + len) >= alloc)
+    {
+      alloc += LINE_BUF_CHUNK + len;
+      buf = (char *) realloc (buf, alloc + 1);
+    }
+  memcpy (buf + ix, what, len);
+  ix = newix;
+  buf[ix] = '\0';
+}
+
+void
+linebuf::prepend (const char *what, int len)
+{
+  int    buflen;
+  size_t newix;
+
+  if ((newix = ix + len) >= alloc)
+    {
+      alloc += LINE_BUF_CHUNK + len;
+      buf = (char *) realloc (buf, alloc + 1);
+      buf[ix] = '\0';
+    }
+  if ((buflen = strlen (buf)))
+    memmove (buf + len, buf, buflen + 1);
+  else
+    buf[newix] = '\0';
+  memcpy (buf, what, len);
+  ix = newix;
+}
+
+void
+make_command_line (linebuf & one_line, char **argv)
+{
+  for (; *argv; argv++)
+    {
+      char *p = NULL;
+      const char *a = *argv;
+
+      int len = strlen (a);
+      if (len != 0 && !(p = strpbrk (a, " \t\n\r\"")))
+        one_line.add (a, len);
+      else
+        {
+          one_line.add ("\"", 1);
+          for (; p; a = p, p = strchr (p, '"'))
+            {
+              one_line.add (a, ++p - a);
+              if (p[-1] == '"')
+                one_line.add ("\"", 1);
+            }
+          if (*a)
+            one_line.add (a);
+          one_line.add ("\"", 1);
+        }
+      one_line.add (" ", 1);
+    }
+
+  if (one_line.ix)
+    one_line.buf[one_line.ix - 1] = '\0';
+  else
+    one_line.add ("", 1);
+}
+
+BOOL WINAPI
+ctrl_c (DWORD)
+{
+  static int tic = 1;
+
+  if ((tic ^= 1) && !GenerateConsoleCtrlEvent (CTRL_C_EVENT, 0))
+    error (0, "couldn't send CTRL-C to child, win32 error %d\n",
+           GetLastError ());
+  return TRUE;
+}
+
+/* Set up interfaces to Cygwin internal funcs and path.cc helper funcs. */
+extern "C" {
+uintptr_t cygwin_internal (int, ...);
+WCHAR cygwin_dll_path[32768];
+}
+
+#define DEBUG_PROCESS_DETACH_ON_EXIT    0x00000001
+#define DEBUG_PROCESS_ONLY_THIS_PROCESS 0x00000002
+
+void
+attach_process (pid_t pid)
+{
+  child_pid = pid < 0 ? (DWORD) -pid :
+        (DWORD) cygwin_internal (CW_CYGWIN_PID_TO_WINPID, pid);
+
+  if (!DebugActiveProcess (child_pid))
+    error (0, "couldn't attach to pid %d for debugging", child_pid);
+
+  if (forkprofile)
+    {
+      HANDLE h = OpenProcess (PROCESS_ALL_ACCESS, FALSE, child_pid);
+
+      if (h)
+        {
+          /* Try to turn off DEBUG_ONLY_THIS_PROCESS so we can follow forks. */
+          ULONG DebugFlags = DEBUG_PROCESS_DETACH_ON_EXIT;
+          NTSTATUS status = NtSetInformationProcess (h, ProcessDebugFlags,
+                                        &DebugFlags, sizeof (DebugFlags));
+          if (!NT_SUCCESS (status))
+            warn (0, "Could not clear DEBUG_ONLY_THIS_PROCESS (%x), "
+                  "will not trace child processes", status);
+
+          CloseHandle (h);
+        }
+    }
+
+  return;
+}
+
+void
+create_child (char **argv)
+{
+  DWORD               flags;
+  linebuf             one_line;
+  PROCESS_INFORMATION pi;
+  BOOL                ret;
+  STARTUPINFO         si;
+
+  if (strchr (*argv, '/'))
+      *argv = cygpath (*argv, NULL);
+  memset (&si, 0, sizeof (si));
+  si.cb = sizeof (si);
+
+  flags = CREATE_DEFAULT_ERROR_MODE
+          | (forkprofile ? DEBUG_PROCESS : DEBUG_ONLY_THIS_PROCESS);
+  if (new_window)
+    flags |= CREATE_NEW_CONSOLE | CREATE_NEW_PROCESS_GROUP;
+
+  make_command_line (one_line, argv);
+
+  SetConsoleCtrlHandler (NULL, 0);
+
+  const char *cygwin_env = getenv ("CYGWIN");
+  const char *space;
+
+  if (cygwin_env && strlen (cygwin_env) <= 256) /* sanity check */
+    space = " ";
+  else
+    space = cygwin_env = "";
+
+  char *newenv = (char *) malloc (sizeof ("CYGWIN=noglob") +
+                                  strlen (space) + strlen (cygwin_env));
+  sprintf (newenv, "CYGWIN=noglob%s%s", space, cygwin_env);
+  putenv (newenv);
+  ret = CreateProcess (0, one_line.buf, /* command line */
+                       NULL,    /* Security */
+                       NULL,    /* thread */
+                       TRUE,    /* inherit handles */
+                       flags,   /* start flags */
+                       NULL,    /* default environment */
+                       NULL,    /* current directory */
+                       &si, &pi);
+  if (!ret)
+    error (0, "error creating process %s, (error %d)", *argv,
+           GetLastError ());
+
+  CloseHandle (pi.hThread);
+  CloseHandle (pi.hProcess);
+  child_pid = pi.dwProcessId;
+  SetConsoleCtrlHandler (ctrl_c, 1);
+}
+
+void
+handle_output_debug_string (DWORD pid, OUTPUT_DEBUG_STRING_INFO *ev)
+{
+  char  *buf = (char *) alloca (ev->nDebugStringLength);
+  child *c = get_child (pid);
+
+  if (!c)
+    error (0, "handle_output_debug_string: pid %lu not found\n", pid);
+
+  read_child (buf, ev->nDebugStringLength, ev->lpDebugStringData, c->hproc);
+  if (strncmp (buf, "cYg", 3))
+    { // string is not from Cygwin, it's from the target app; just display it
+      if (ev->fUnicode)
+        note ("%ls", buf);
+      else
+        note ("%s", buf);
+    }
+  //else TODO Possibly decode and display Cygwin-internal debug string
+}
+
+BOOL
+GetFileNameFromHandle (HANDLE hFile, WCHAR pszFilename[MAX_PATH+1])
+{
+  BOOL     result = FALSE;
+  ULONG    len = 0;
+  OBJECT_NAME_INFORMATION *ntfn = (OBJECT_NAME_INFORMATION *) alloca (65536);
+  NTSTATUS status = NtQueryObject (hFile, ObjectNameInformation,
+                                   ntfn, 65536, &len);
+  if (NT_SUCCESS (status))
+    {
+      PWCHAR win32path = ntfn->Name.Buffer;
+      win32path[ntfn->Name.Length / sizeof (WCHAR)] = L'\0';
+
+      /* NtQueryObject returns a native NT path.  (Try to) convert to Win32. */
+      if (drive_map)
+        win32path = (PWCHAR) cygwin_internal (CW_MAP_DRIVE_MAP, drive_map,
+                                              win32path);
+      pszFilename[0] = L'\0';
+      wcsncat (pszFilename, win32path, MAX_PATH);
+      result = TRUE;
+    }
+
+  return result;
+}
+
+char *
+cygwin_pid (DWORD winpid)
+{
+  static char  buf[48];
+  DWORD        cygpid;
+  static DWORD max_cygpid = 0;
+
+  if (!max_cygpid)
+    max_cygpid = (DWORD) cygwin_internal (CW_MAX_CYGWIN_PID);
+
+  cygpid = (DWORD) cygwin_internal (CW_WINPID_TO_CYGWIN_PID, winpid);
+
+  if (cygpid >= max_cygpid)
+    snprintf (buf, sizeof buf, "%u", winpid);
+  else
+    snprintf (buf, sizeof buf, "%u (pid: %u)", winpid, cygpid);
+  return buf;
+}
+
+DWORD
+profile1 (FILE *ofile, pid_t pid)
+{
+  DEBUG_EVENT ev;
+  DWORD       res = 0;
+
+  SetThreadPriority (GetCurrentThread (), THREAD_PRIORITY_HIGHEST);
+  while (1)
+    {
+      BOOL debug_event = WaitForDebugEvent (&ev, INFINITE);
+      DWORD status = DBG_CONTINUE;
+
+      if (!debug_event)
+        continue;
+
+      /* Usually continue event here so child resumes while we process event. */
+      if (ev.dwDebugEventCode != EXCEPTION_DEBUG_EVENT &&
+          ev.dwDebugEventCode != OUTPUT_DEBUG_STRING_EVENT)
+        debug_event = ContinueDebugEvent (ev.dwProcessId, ev.dwThreadId, status);
+
+      switch (ev.dwDebugEventCode)
+        {
+        case CREATE_PROCESS_DEBUG_EVENT:
+          WCHAR exename[MAX_PATH+1];
+
+          if (!GetFileNameFromHandle (ev.u.CreateProcessInfo.hFile, exename))
+            wcscpy (exename, L"(unknown)");
+          if (events)
+            {
+              note ("--- Process %s created from %ls\n",
+                    cygwin_pid (ev.dwProcessId), exename);
+              note ("--- Process %s thread %lu created at %p\n",
+                    cygwin_pid (ev.dwProcessId), ev.dwThreadId,
+                    ev.u.CreateProcessInfo.lpStartAddress);
+            }
+          if (ev.u.CreateProcessInfo.hFile)
+            CloseHandle (ev.u.CreateProcessInfo.hFile);
+          add_child (ev.dwProcessId, wcsdup (exename),
+                     ev.u.CreateProcessInfo.lpBaseOfImage,
+                     ev.u.CreateProcessInfo.hProcess);
+          add_thread (ev.dwProcessId, ev.dwThreadId,
+                      ev.u.CreateProcessInfo.hThread, wcsdup (exename));
+          break;
+
+        case CREATE_THREAD_DEBUG_EVENT:
+          if (events)
+            note ("--- Process %s thread %lu created at %p\n",
+                  cygwin_pid (ev.dwProcessId), ev.dwThreadId,
+                  ev.u.CreateThread.lpStartAddress);
+          add_thread (ev.dwProcessId, ev.dwThreadId,
+                      ev.u.CreateThread.hThread, NULL);
+          break;
+
+        case LOAD_DLL_DEBUG_EVENT:
+          WCHAR dllname[MAX_PATH+1];
+
+          /* lpImageName is not always populated, so find the filename for
+             hFile instead. */
+          if (!GetFileNameFromHandle (ev.u.LoadDll.hFile, dllname))
+            wcscpy (dllname, L"(unknown)");
+
+          if (events)
+              note ("--- Process %s loaded %ls at %p\n",
+                    cygwin_pid (ev.dwProcessId), dllname,
+                    ev.u.LoadDll.lpBaseOfDll);
+          add_span (ev.dwProcessId, wcsdup (dllname),
+                    ev.u.LoadDll.lpBaseOfDll, ev.u.LoadDll.hFile);
+
+          if (ev.u.LoadDll.hFile)
+            CloseHandle (ev.u.LoadDll.hFile);
+          break;
+
+        case UNLOAD_DLL_DEBUG_EVENT:
+          if (events)
+            note ("--- Process %s unloaded DLL at %p\n",
+                  cygwin_pid (ev.dwProcessId), ev.u.UnloadDll.lpBaseOfDll);
+          break;
+
+        case OUTPUT_DEBUG_STRING_EVENT:
+          handle_output_debug_string (ev.dwProcessId, &ev.u.DebugString);
+          status = DBG_EXCEPTION_HANDLED;
+          debug_event = ContinueDebugEvent (ev.dwProcessId,
+                                            ev.dwThreadId, status);
+          break;
+
+        case EXIT_PROCESS_DEBUG_EVENT:
+          if (events)
+            note ("--- Process %s exited with status 0x%lx\n",
+                  cygwin_pid (ev.dwProcessId), ev.u.ExitProcess.dwExitCode);
+          res = ev.u.ExitProcess.dwExitCode;
+          remove_child (ev.dwProcessId);
+          break;
+
+        case EXIT_THREAD_DEBUG_EVENT:
+          if (events)
+            note ("--- Process %s thread %lu exited with status 0x%lx\n",
+                  cygwin_pid (ev.dwProcessId), ev.dwThreadId,
+                  ev.u.ExitThread.dwExitCode);
+          remove_thread (ev.dwProcessId, ev.dwThreadId);
+          break;
+
+        case EXCEPTION_DEBUG_EVENT:
+          status = DBG_EXCEPTION_HANDLED;
+          switch (ev.u.Exception.ExceptionRecord.ExceptionCode)
+            {
+            case MS_VC_EXCEPTION:
+              //TODO Decode exception info to get thread name; set it internally
+              // fall thru
+
+            case STATUS_BREAKPOINT:
+              break;
+
+#ifdef __x86_64__
+            case STATUS_GCC_THROW:
+            case STATUS_GCC_UNWIND:
+            case STATUS_GCC_FORCED:
+              status = DBG_EXCEPTION_NOT_HANDLED;
+              break;
+#endif
+
+            default:
+              status = DBG_EXCEPTION_NOT_HANDLED;
+              if (ev.u.Exception.dwFirstChance)
+                note ("--- Process %s thread %lu exception %08x at %p\n",
+                      cygwin_pid (ev.dwProcessId), ev.dwThreadId,
+                      ev.u.Exception.ExceptionRecord.ExceptionCode,
+                      ev.u.Exception.ExceptionRecord.ExceptionAddress);
+              break;
+            }
+          debug_event = ContinueDebugEvent (ev.dwProcessId,
+                                            ev.dwThreadId, status);
+          break;
+        }
+
+      if (!debug_event)
+        error (0, "couldn't continue debug event, windows error %d",
+               GetLastError ());
+      if (!numprocesses)
+        break;
+    }
+
+  return res;
+}
+
+DWORD
+doprofile (FILE *ofile, pid_t pid, char **argv)
+{
+  if (pid)
+    attach_process (pid);
+  else
+    create_child (argv);
+
+  return profile1 (ofile, pid);
+}
+
+struct option longopts[] = {
+  {"debug",       no_argument,       NULL, 'd'},
+  {"events",      no_argument,       NULL, 'e'},
+  {"help",        no_argument,       NULL, 'h'},
+  {"new-window",  no_argument,       NULL, 'w'},
+  {"output",      required_argument, NULL, 'o'},
+  {"pid",         required_argument, NULL, 'p'},
+  {"fork-profile",no_argument,       NULL, 'f'},
+  {"sample-rate", required_argument, NULL, 's'},
+  {"verbose",     no_argument,       NULL, 'v'},
+  {"version",     no_argument,       NULL, 'V'},
+  {NULL,          0,                 NULL, 0  }
+};
+
+const char *const opts = "+dehfo:p:s:vVw";
+
+void __attribute__ ((__noreturn__))
+print_version ()
+{
+  char *year_of_build = strrchr (__DATE__, ' ') + 1;
+  printf ("profiler (cygwin) %d.%d.%d\n"
+          "IP-Sampling Profiler\n"
+          "Copyright (C) %s%s Cygwin Authors\n"
+          "This is free software; see the source for copying conditions.  "
+          "There is NO\nwarranty; not even for MERCHANTABILITY or FITNESS "
+          "FOR A PARTICULAR PURPOSE.\n",
+          CYGWIN_VERSION_DLL_MAJOR / 1000,
+          CYGWIN_VERSION_DLL_MAJOR % 1000,
+          CYGWIN_VERSION_DLL_MINOR,
+          strncmp (year_of_build, "2021", 4) ? "2021 - " : "",
+          year_of_build);
+  exit (0);
+}
+
+int
+main2 (int argc, char **argv)
+{
+  int    opt;
+  pid_t  pid = 0;
+  char  *ptr;
+  DWORD  ret = 0;
+
+  _setmode (1, O_BINARY);
+  _setmode (2, O_BINARY);
+
+  if (!(pgm = strrchr (*argv, '\\')) && !(pgm = strrchr (*argv, '/')))
+    pgm = *argv;
+  else
+    pgm++;
+
+  while ((opt = getopt_long (argc, argv, opts, longopts, NULL)) != EOF)
+    switch (opt)
+      {
+      case 'd':
+        debugging ^= 1;
+        if (debugging)
+          verbose = events = 1; // debugging turns these on too
+        break;
+
+      case 'e':
+        events ^= 1;
+        events |= debugging; // debugging turns on events too
+        break;
+
+      case 'f':
+        forkprofile ^= 1;
+        break;
+
+      case 'h':
+        /* Print help and exit. */
+        usage (ofile);
+
+      case 'o':
+        if ((ofile = fopen (cygpath (optarg, NULL), "wb")) == NULL)
+          error (1, "can't open %s", optarg);
+#ifdef F_SETFD
+        (void) fcntl (fileno (ofile), F_SETFD, 0);
+#endif
+        break;
+
+      case 'p':
+        pid = strtoul (optarg, NULL, 10);
+        break;
+
+      case 's':
+        samplerate = strtoul (optarg, NULL, 10);
+        if (samplerate < 1 || samplerate > 1000)
+          error (0, "sample rate must be between 1 and 1000 inclusive");
+        break;
+
+      case 'v':
+        verbose ^= 1;
+        verbose |= debugging; // debugging turns on verbose too
+        break;
+
+      case 'V':
+        /* Print version info and exit. */
+        print_version ();
+
+      case 'w':
+        new_window ^= 1;
+        break;
+
+      default:
+        note ("Try `%s --help' for more information.\n", pgm);
+        exit (1);
+      }
+
+  if (pid && argv[optind])
+    error (0, "cannot provide both a command line and a process id");
+
+  if (!pid && !argv[optind])
+    error (0, "must provide either a command line or a process id");
+
+  /* Honor user-supplied gmon file name prefix, if available. */
+  ptr = getenv ("GMON_OUT_PREFIX");
+  if (ptr && strlen (ptr) > 0)
+    prefix = ptr;
+
+  drive_map = (void *) cygwin_internal (CW_ALLOC_DRIVE_MAP);
+  ret = doprofile (ofile, pid, argv + optind);
+  if (drive_map)
+    cygwin_internal (CW_FREE_DRIVE_MAP, drive_map);
+
+  if (ofile && ofile != stdout)
+    fclose (ofile);
+  return (ret);
+}
+
+int
+main (int argc, char **argv)
+{
+  /* Make sure to have room for the _cygtls area *and* to initialize it.
+   * This is required to make sure cygwin_internal calls into Cygwin work
+   * reliably.  This problem has been noticed under AllocationPreference
+   * registry setting to 0x100000 (TOP_DOWN).
+   */
+  char buf[CYGTLS_PADSIZE];
+
+  RtlSecureZeroMemory (buf, sizeof (buf));
+  exit (main2 (argc, argv));
+}
-- 
2.31.1


^ permalink raw reply	[flat|nested] 9+ messages in thread

* [PATCH 2/3] Cygwin: New tool: gmondump
  2021-07-16  4:49 [PATCH 1/3] Cygwin: New tool: profiler Mark Geisert
@ 2021-07-16  4:49 ` Mark Geisert
  2021-07-16  4:49 ` [PATCH 3/3] Cygwin: updates to wire in profiler, gmondump Mark Geisert
  2021-07-19 10:04 ` [PATCH 1/3] Cygwin: New tool: profiler Corinna Vinschen
  2 siblings, 0 replies; 9+ messages in thread
From: Mark Geisert @ 2021-07-16  4:49 UTC (permalink / raw)
  To: cygwin-patches

This new tool was formerly part of 'profiler' but was spun out thanks to
Jon T's reasonable review comment.  Gmondump is more of a debugging tool
than something users might have need for.  Users would more likely use
gprof to make use of symbolic info like function names and source line
numbers.

---
 winsup/utils/gmondump.c | 255 ++++++++++++++++++++++++++++++++++++++++
 1 file changed, 255 insertions(+)
 create mode 100644 winsup/utils/gmondump.c

diff --git a/winsup/utils/gmondump.c b/winsup/utils/gmondump.c
new file mode 100644
index 000000000..e469f01f1
--- /dev/null
+++ b/winsup/utils/gmondump.c
@@ -0,0 +1,255 @@
+/*
+    gmondump.c
+    Displays summary info about given profile data file(s).
+
+    Written by Mark Geisert <mark@maxrnd.com>.
+
+    This file is part of Cygwin.
+
+    This software is a copyrighted work licensed under the terms of the
+    Cygwin license.  Please consult the file "CYGWIN_LICENSE" for details.
+*/
+
+#include <errno.h>
+#include <fcntl.h>
+#include <getopt.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <sys/stat.h>
+#include "cygwin/version.h"
+
+typedef unsigned short ushort;
+typedef uint16_t u_int16_t; // Non-standard sized type needed by ancient gmon.h
+#include "gmon.h"
+
+FILE       *ofile;
+const char *pgm = "gmondump";
+int         verbose = 0;
+
+void __attribute__ ((__noreturn__))
+usage (FILE *where)
+{
+  fprintf (where, "\
+Usage: %s [OPTIONS] FILENAME...\n\
+\n\
+Display formatted contents of profile data file(s).\n\
+Such files usually have names starting with \"gmon.out\".\n\
+OPTIONS are:\n\
+\n\
+  -h, --help             Display usage information and exit\n\
+  -v, --verbose          Display more file details (toggle: default false)\n\
+  -V, --version          Display version information and exit\n\
+\n", pgm);
+
+  exit (where == stderr ? 1 : 0 );
+}
+
+void
+note (const char *fmt, ...)
+{
+  va_list args;
+  char    buf[4096];
+
+  va_start (args, fmt);
+  vsprintf (buf, fmt, args);
+  va_end (args);
+
+  fputs (buf, ofile);
+  fflush (ofile);
+}
+
+void
+warn (int geterrno, const char *fmt, ...)
+{
+  va_list args;
+  char    buf[4096];
+
+  va_start (args, fmt);
+  sprintf (buf, "%s: ", pgm);
+  vsprintf (strchr (buf, '\0'), fmt, args);
+  va_end (args);
+  if (geterrno)
+    perror (buf);
+  else
+    {
+      fputs (buf, ofile);
+      fputs ("\n", ofile);
+      fflush (ofile);
+    }
+}
+
+void __attribute__ ((noreturn))
+error (int geterrno, const char *fmt, ...)
+{
+  va_list args;
+
+  va_start (args, fmt);
+  warn (geterrno, fmt, args);
+  va_end (args);
+
+  exit (1);
+}
+
+void
+gmondump1 (char *filename)
+{
+  ushort    *bucket = NULL;
+  int        fd;
+  struct gmonhdr hdr;
+  int        hitbuckets;
+  int        hitcount;
+  int        numbuckets;
+  int        numrawarcs;
+  struct rawarc *rawarc = NULL;
+  int        res;
+  struct stat stat;
+
+  fd = open (filename, O_RDONLY | O_BINARY);
+  if (fd < 0)
+    {
+      note ("file%s %s couldn't be opened; continuing\n",
+            strchr (filename, '*') ? "s" : "", filename);
+      return;
+    }
+
+  /* Read and sanity-check what should be a gmon header. */
+  res = fstat (fd, &stat);
+  if (res < 0)
+    goto notgmon;
+  if (S_IFREG != (stat.st_mode & S_IFMT))
+    goto notgmon;
+  res = read (fd, &hdr, sizeof (hdr));
+  if (res != sizeof (hdr))
+    goto notgmon;
+  if (hdr.lpc >= hdr.hpc)
+    goto notgmon;
+  numbuckets = (hdr.ncnt - sizeof (hdr)) / sizeof (short);
+  if (numbuckets != (hdr.hpc - hdr.lpc) / 4)
+    goto notgmon;
+  numrawarcs = 0;
+  if (stat.st_size != hdr.ncnt)
+    {
+      numrawarcs = stat.st_size - hdr.ncnt;
+      if (numrawarcs !=
+          (int) sizeof (rawarc) * (numrawarcs / (int) sizeof (rawarc)))
+        goto notgmon;
+      numrawarcs /= (int) sizeof (rawarc);
+    }
+
+  /* Looks good, so read and display the profiling info. */
+  bucket = (ushort *) calloc (numbuckets, sizeof (ushort));
+  res = read (fd, bucket, hdr.ncnt - sizeof (hdr));
+  if (res != hdr.ncnt - (int) sizeof (hdr))
+    goto notgmon;
+  hitcount = hitbuckets = 0;
+  for (res = 0; res < numbuckets; ++bucket, ++res)
+    if (*bucket)
+      {
+        ++hitbuckets;
+        hitcount += *bucket;
+      }
+  bucket -= numbuckets;
+
+  note ("file %s, gmon version 0x%x, sample rate %d\n",
+        filename, hdr.version, hdr.profrate);
+  note ("  address range 0x%p..0x%p\n", hdr.lpc, hdr.hpc);
+  note ("  numbuckets %d, hitbuckets %d, hitcount %d, numrawarcs %d\n",
+        numbuckets, hitbuckets, hitcount, numrawarcs);
+
+  /* If verbose is set, display contents of buckets and rawarcs arrays. */
+  if (verbose)
+    {
+      if (hitbuckets)
+        note ("  bucket data follows...\n");
+      char *addr = (char *) hdr.lpc;
+      int   incr = (hdr.hpc - hdr.lpc) / numbuckets;
+      for (res = 0; res < numbuckets; ++bucket, ++res, addr += incr)
+        if (*bucket)
+          note ("    address 0x%p, hitcount %d\n", addr, *bucket);
+      bucket -= numbuckets;
+
+      if (numrawarcs)
+        {
+          rawarc = (struct rawarc *) calloc (numrawarcs, sizeof (rawarc));
+          res = read (fd, rawarc, numrawarcs * (int) sizeof (rawarc));
+          if (res != numrawarcs * (int) sizeof (rawarc))
+            error (0, "unable to read rawarc data");
+          note ("  rawarc data follows...\n");
+          for (res = 0; res < numrawarcs; ++rawarc, ++res)
+            note ("    from 0x%p, self 0x%p, count %d\n",
+                  rawarc->raw_frompc, rawarc->raw_selfpc, rawarc->raw_count);
+        }
+    }
+
+  note ("\n");
+  if (0)
+    {
+notgmon:
+      note ("file %s isn't a profile data file; continuing\n", filename);
+    }
+  if (rawarc)
+    free (rawarc);
+  if (bucket)
+    free (bucket);
+  close (fd);
+}
+
+struct option longopts[] = {
+  {"help",    no_argument, NULL, 'h'},
+  {"verbose", no_argument, NULL, 'v'},
+  {"version", no_argument, NULL, 'V'},
+  {NULL,      0,           NULL, 0  }
+};
+
+const char *const opts = "+hvV";
+
+void __attribute__ ((__noreturn__))
+print_version ()
+{
+  char *year_of_build = strrchr (__DATE__, ' ') + 1;
+  printf ("gmondump (cygwin) %d.%d.%d\n"
+          "Profiler data file viewer\n"
+          "Copyright (C) %s%s Cygwin Authors\n"
+          "This is free software; see the source for copying conditions.  "
+          "There is NO\nwarranty; not even for MERCHANTABILITY or FITNESS "
+          "FOR A PARTICULAR PURPOSE.\n",
+          CYGWIN_VERSION_DLL_MAJOR / 1000,
+          CYGWIN_VERSION_DLL_MAJOR % 1000,
+          CYGWIN_VERSION_DLL_MINOR,
+          strncmp (year_of_build, "2021", 4) ? "2021 - " : "",
+          year_of_build);
+  exit (0);
+}
+
+int
+main(int argc, char **argv)
+{
+  ofile = stdout;
+  int opt;
+
+  while ((opt = getopt_long (argc, argv, opts, longopts, NULL)) != EOF)
+    switch (opt)
+      {
+      case 'h':
+        /* Print help and exit. */
+        usage (ofile);
+
+      case 'v':
+        verbose ^= 1;
+        break;
+
+      case 'V':
+        /* Print version and exit. */
+        print_version ();
+
+      default:
+        ;
+      }
+
+  for (int i = optind; i < argc; i++)
+    gmondump1 (argv[i]);
+
+  return 0;
+}
-- 
2.31.1


^ permalink raw reply	[flat|nested] 9+ messages in thread

* [PATCH 3/3] Cygwin: updates to wire in profiler, gmondump
  2021-07-16  4:49 [PATCH 1/3] Cygwin: New tool: profiler Mark Geisert
  2021-07-16  4:49 ` [PATCH 2/3] Cygwin: New tool: gmondump Mark Geisert
@ 2021-07-16  4:49 ` Mark Geisert
  2021-07-19 10:04 ` [PATCH 1/3] Cygwin: New tool: profiler Corinna Vinschen
  2 siblings, 0 replies; 9+ messages in thread
From: Mark Geisert @ 2021-07-16  4:49 UTC (permalink / raw)
  To: cygwin-patches

These are updates to wire into the build tree the new tools profiler and
gmondump, and to supply documentation for the tools.

The documentation for profiler and ssp now mention each other but do not
discuss their similarities or differences.  That will be handled in a
future update to the "Profiling Cygwin Programs" section of the Cygwin
User's Guide, to be supplied.

---
 winsup/cygwin/release/3.2.1 |   7 ++
 winsup/doc/utils.xml        | 123 ++++++++++++++++++++++++++++++++++++
 winsup/utils/Makefile.am    |   5 ++
 3 files changed, 135 insertions(+)

diff --git a/winsup/cygwin/release/3.2.1 b/winsup/cygwin/release/3.2.1
index 99c65ce30..4f4db622a 100644
--- a/winsup/cygwin/release/3.2.1
+++ b/winsup/cygwin/release/3.2.1
@@ -1,6 +1,13 @@
 What's new:
 -----------
 
+- An IP-sampling profiler named 'profiler' has been added.  It can be used
+  to profile any Cygwin program along with any DLLs loaded.
+
+- A new tool 'gmondump' has been added.  It can dump the raw information
+  of any "gmon.out" file created by profiler, ssp, or use of the gcc/g++
+  option '-pg'.  (Continue using gprof to get symbolic profile displays.)
+
 
 What changed:
 -------------
diff --git a/winsup/doc/utils.xml b/winsup/doc/utils.xml
index 1d9b8488c..0b7b5d0ea 100644
--- a/winsup/doc/utils.xml
+++ b/winsup/doc/utils.xml
@@ -793,6 +793,56 @@ line separates the ACLs for each file.
     </refsect1>
   </refentry>
 
+  <refentry id="gmondump">
+    <refmeta>
+      <refentrytitle>gmondump</refentrytitle>
+      <manvolnum>1</manvolnum>
+      <refmiscinfo class="manual">Cygwin Utilities</refmiscinfo>
+    </refmeta>
+
+    <refnamediv>
+      <refname>gmondump</refname>
+      <refpurpose>Display formatted contents of profile data files</refpurpose>
+    </refnamediv>
+
+    <refsynopsisdiv>
+    <screen>
+gmondump [OPTION]... FILENAME...
+    </screen>
+    </refsynopsisdiv>
+
+    <refsect1 id="gmondump-options">
+      <title>Options</title>
+      <screen>
+  -h, --help             Display usage information and exit
+  -v, --verbose          Display more file details (toggle: default false)
+  -V, --version          Display version information and exit
+</screen>
+    </refsect1>
+
+    <refsect1 id="gmondump-desc">
+      <title>Description</title>
+    <para>The <command>gmondump</command> utility displays the contents of
+      one or more profile data files. Such files usually have names starting
+      with "gmon.out" and are created by a profiling program such as
+      <command>profiler</command> or <command>ssp</command>. Compiling your
+      gcc/g++ programs with option <literal>-pg</literal> also works.</para>
+    <para> By default, summary information is shown. You can use the
+      option <literal>-v</literal> to get more detailed displays.</para>
+    <para>Note that <command>gmondump</command> just displays the raw data;
+      one would usually use <command>gprof</command> to display the data in
+      a useful form incorporating symbolic info such as function names and
+      source line numbers.</para>
+    <para>Here is an example of <command>gmondump</command> operation:</para>
+<screen>
+$ gmondump gmon.out.21900.zstd.exe
+file gmon.out.21900.zstd.exe, gmon version 0x51879, sample rate 100
+  address range 0x0x100401000..0x0x1004cc668
+  numbuckets 208282, hitbuckets 1199, hitcount 12124, numrawarcs 0
+</screen>
+    </refsect1>
+  </refentry>
+
   <refentry id="kill">
     <refmeta>
       <refentrytitle>kill</refentrytitle>
@@ -2127,6 +2177,75 @@ specifying an empty password.
     </refsect1>
   </refentry>
 
+  <refentry id="profiler">
+    <refmeta>
+      <refentrytitle>profiler</refentrytitle>
+      <manvolnum>1</manvolnum>
+      <refmiscinfo class="manual">Cygwin Utilities</refmiscinfo>
+    </refmeta>
+
+    <refnamediv>
+      <refname>profiler</refname>
+      <refpurpose>Sampling profiler of Cygwin programs with their DLLs</refpurpose>
+    </refnamediv>
+
+    <refsynopsisdiv>
+    <screen>
+profiler [OPTION]... PROGRAM [ARG]...
+profiler [OPTION]... -p PID
+    </screen>
+    </refsynopsisdiv>
+
+    <refsect1 id="profiler-options">
+      <title>Options</title>
+      <screen>
+  -d, --debug            Display debugging messages (toggle: default false)
+  -e, --events           Display Windows DEBUG_EVENTS (toggle: default false)
+  -f, --fork-profile     Profiles child processes (toggle: default false)
+  -h, --help             Display usage information and exit
+  -o, --output=FILENAME  Write output to file FILENAME rather than stdout
+  -p, --pid=N            Attach to running program with Cygwin pid N
+                         ...                    or with Windows pid -N
+  -s, --sample-rate=N    Set IP sampling rate to N Hz (default 100)
+  -v, --verbose          Display more status messages (toggle: default false)
+  -V, --version          Display version information and exit
+  -w, --new-window       Launch given command in a new window
+</screen>
+    </refsect1>
+
+    <refsect1 id="profiler-desc">
+      <title>Description</title>
+    <para>The <command>profiler</command> utility executes a given program, and
+      optionally the children of that program, collecting the location of the
+      CPU instruction pointer (IP) many times per second. This gives a profile
+      of the program's execution, showing where the most time is being spent.
+      This profiling technique is called "IP sampling".</para>
+
+    <para>A novel feature of <command>profiler</command> is that time spent in
+      DLLs loaded with or by your program is profiled too. You use
+      <command>gprof</command> to process and display the resulting profile
+      information. In this fashion you can determine whether your own code,
+      the Cygwin DLL, or another DLL has "hot spots" that might benefit from
+      tuning.</para>
+
+    <para>(See also <command>ssp</command>, another profiler that
+      operates in a different fashion: stepping by instruction. This can
+      provide a different view on your program's operation.)</para>
+
+    <para>Here is an example of <command>profiler</command> operation:</para>
+<screen>
+$ profiler du -khs .
+22G     .
+97 samples across 83 buckets written to gmon.out.5908.cygwin1.dll
+4 samples across 4 buckets written to gmon.out.5908.KernelBase.dll
+1 sample across 1 bucket written to gmon.out.5908.kernel32.dll
+7318 samples across 42 buckets written to gmon.out.5908.ntdll.dll
+5 samples across 4 buckets written to gmon.out.5908.du.exe
+</screen>
+    </refsect1>
+
+  </refentry>
+
   <refentry id="ps">
     <refmeta>
       <refentrytitle>ps</refentrytitle>
@@ -2775,6 +2894,10 @@ Example: ssp 0x401000 0x403000 hello.exe
       <command>gprof</command> will claim the values are seconds, they really
       are instruction counts. More on that later. </para>
 
+    <para>(See also <command>profiler</command>, another profiler that
+      operates in a different fashion: IP sampling. This can provide a
+      different view on your program's operation.)</para>
+
     <para> Because the SSP was originally designed to profile the Cygwin DLL,
       it does not automatically select a block of code to report statistics on.
       You must specify the range of memory addresses to keep track of manually,
diff --git a/winsup/utils/Makefile.am b/winsup/utils/Makefile.am
index 9a846e39d..135e6143c 100644
--- a/winsup/utils/Makefile.am
+++ b/winsup/utils/Makefile.am
@@ -21,6 +21,7 @@ bin_PROGRAMS = \
 	gencat \
 	getconf \
 	getfacl \
+	gmondump \
 	kill \
 	ldd \
 	locale \
@@ -31,6 +32,7 @@ bin_PROGRAMS = \
 	mount \
 	passwd \
 	pldd \
+	profiler \
 	regtool \
 	setfacl \
 	setmetamode \
@@ -54,6 +56,7 @@ ldd_SOURCES = ldd.cc
 locale_SOURCES = locale.cc
 minidumper_SOURCES = minidumper.cc
 mount_SOURCES = mount.cc path.cc
+profiler_SOURCES = profiler.cc path.cc
 cygps_SOURCES = ps.cc
 regtool_SOURCES = regtool.cc
 umount_SOURCES = umount.cc
@@ -79,6 +82,8 @@ ldd_LDADD = $(LDADD) -lpsapi -lntdll
 mount_CXXFLAGS = -DFSTAB_ONLY $(AM_CXXFLAGS)
 minidumper_LDADD = $(LDADD) -ldbghelp
 pldd_LDADD = $(LDADD) -lpsapi
+profiler_CXXFLAGS = -I$(srcdir) -idirafter ${top_srcdir}/cygwin -idirafter ${top_srcdir}/cygwin/include $(AM_CXXFLAGS)
+profiler_LDADD = $(LDADD) -lntdll
 cygps_LDADD = $(LDADD) -lpsapi -lntdll
 
 if CROSS_BOOTSTRAP
-- 
2.31.1


^ permalink raw reply	[flat|nested] 9+ messages in thread

* Re: [PATCH 1/3] Cygwin: New tool: profiler
  2021-07-16  4:49 [PATCH 1/3] Cygwin: New tool: profiler Mark Geisert
  2021-07-16  4:49 ` [PATCH 2/3] Cygwin: New tool: gmondump Mark Geisert
  2021-07-16  4:49 ` [PATCH 3/3] Cygwin: updates to wire in profiler, gmondump Mark Geisert
@ 2021-07-19 10:04 ` Corinna Vinschen
  2021-07-19 14:23   ` Jon Turney
  2 siblings, 1 reply; 9+ messages in thread
From: Corinna Vinschen @ 2021-07-19 10:04 UTC (permalink / raw)
  To: Mark Geisert; +Cc: cygwin-patches, Jon TURNEY

Hi Matt,

On Jul 15 21:49, Mark Geisert wrote:
> The new tool formerly known as cygmon is renamed to 'profiler'.  For the
> name I considered 'ipsampler' and could not think of any others.  I'm open
> to a different name if any is suggested.
> 
> I decided that a discussion of the pros and cons of this profiler vs the
> existing ssp should probably be in the "Profiling Cygwin Programs" section
> of the Cygwin User's Guide rather than in the help for either.  That
> material will be supplied at some point.
> 
> CONTEXT buffers are made child-specific and thus thread-specific since
> there is one profiler thread for each child program being profiled.
> 
> The SetThreadPriority() warning comment has been expanded.
> 
> chmod() works on Cygwin so the "//XXX ineffective" comment is gone.
> 
> I decided to make the "sample all executable sections" and "sample
> dynamically generated code" suggestions simply expanded comments for now.
> 
> The profiler program is now a Cygwin exe rather than a native exe.

The patchset LGTM, but for the details I'd like jturney to have a look
and approve it eventually.


Thanks,
Corinna

^ permalink raw reply	[flat|nested] 9+ messages in thread

* Re: [PATCH 1/3] Cygwin: New tool: profiler
  2021-07-19 10:04 ` [PATCH 1/3] Cygwin: New tool: profiler Corinna Vinschen
@ 2021-07-19 14:23   ` Jon Turney
  2021-07-19 15:43     ` Jon Turney
  0 siblings, 1 reply; 9+ messages in thread
From: Jon Turney @ 2021-07-19 14:23 UTC (permalink / raw)
  To: Cygwin Patches

On 19/07/2021 11:04, Corinna Vinschen wrote:
> Hi Matt,
> 
> On Jul 15 21:49, Mark Geisert wrote:
>> The new tool formerly known as cygmon is renamed to 'profiler'.  For the
>> name I considered 'ipsampler' and could not think of any others.  I'm open
>> to a different name if any is suggested.
>>
>> I decided that a discussion of the pros and cons of this profiler vs the
>> existing ssp should probably be in the "Profiling Cygwin Programs" section
>> of the Cygwin User's Guide rather than in the help for either.  That
>> material will be supplied at some point.
>>
>> CONTEXT buffers are made child-specific and thus thread-specific since
>> there is one profiler thread for each child program being profiled.
>>
>> The SetThreadPriority() warning comment has been expanded.
>>
>> chmod() works on Cygwin so the "//XXX ineffective" comment is gone.
>>
>> I decided to make the "sample all executable sections" and "sample
>> dynamically generated code" suggestions simply expanded comments for now.
>>
>> The profiler program is now a Cygwin exe rather than a native exe.
> 
> The patchset LGTM, but for the details I'd like jturney to have a look
> and approve it eventually.

Thanks.  I applied these patches.

A few small issues you might consider addressing in follow-ups.

> +
> +/* Set up interfaces to Cygwin internal funcs and path.cc helper funcs. */
> +extern "C" {
> +uintptr_t cygwin_internal (int, ...);

Since this is a now cygwin application, you can include <sys/cygwin.h> 
for this prototype.

> +WCHAR cygwin_dll_path[32768];
> +}

This is unused.

> +int
> +main (int argc, char **argv)
> +{
> +  /* Make sure to have room for the _cygtls area *and* to initialize it.
> +   * This is required to make sure cygwin_internal calls into Cygwin work
> +   * reliably.  This problem has been noticed under AllocationPreference
> +   * registry setting to 0x100000 (TOP_DOWN).
> +   */
> +  char buf[CYGTLS_PADSIZE];
> +
> +  RtlSecureZeroMemory (buf, sizeof (buf));

Since you aren't dynamically loading cygwin1.dll, none of this guff is 
needed.

> +    <refsynopsisdiv>
> +    <screen>
> +gmondump [OPTION]... FILENAME...
> +    </screen>
> +    </refsynopsisdiv>

This says that 1 or more FILENAME are expected, but actually 0 is handled.

So perhaps either document the actual behaviour, or make providing no 
filenames to gmondump an error.

> +    <para>The <command>profiler</command> utility executes a given program, and
> +      optionally the children of that program, collecting the location of the
> +      CPU instruction pointer (IP) many times per second.

I don't think this says what you mean.  It's not optional that the child 
processes are executed, it's optional that they are profiled.

^ permalink raw reply	[flat|nested] 9+ messages in thread

* Re: [PATCH 1/3] Cygwin: New tool: profiler
  2021-07-19 14:23   ` Jon Turney
@ 2021-07-19 15:43     ` Jon Turney
  2021-07-21  8:00       ` [PATCH] Cygwin: fix format warnings in profiler.cc Mark Geisert
  2021-07-21  8:07       ` [PATCH 1/3] Cygwin: New tool: profiler Corinna Vinschen
  0 siblings, 2 replies; 9+ messages in thread
From: Jon Turney @ 2021-07-19 15:43 UTC (permalink / raw)
  To: Cygwin Patches

On 19/07/2021 15:23, Jon Turney wrote:
> On 19/07/2021 11:04, Corinna Vinschen wrote:
>> Hi Matt,
>>
>> On Jul 15 21:49, Mark Geisert wrote:
>>> The new tool formerly known as cygmon is renamed to 'profiler'.  For the
>>> name I considered 'ipsampler' and could not think of any others.  I'm 
>>> open
>>> to a different name if any is suggested.
>>>
>>> I decided that a discussion of the pros and cons of this profiler vs the
>>> existing ssp should probably be in the "Profiling Cygwin Programs" 
>>> section
>>> of the Cygwin User's Guide rather than in the help for either.  That
>>> material will be supplied at some point.
>>>
>>> CONTEXT buffers are made child-specific and thus thread-specific since
>>> there is one profiler thread for each child program being profiled.
>>>
>>> The SetThreadPriority() warning comment has been expanded.
>>>
>>> chmod() works on Cygwin so the "//XXX ineffective" comment is gone.
>>>
>>> I decided to make the "sample all executable sections" and "sample
>>> dynamically generated code" suggestions simply expanded comments for 
>>> now.
>>>
>>> The profiler program is now a Cygwin exe rather than a native exe.
>>
>> The patchset LGTM, but for the details I'd like jturney to have a look
>> and approve it eventually.
> 
> Thanks.  I applied these patches.
> 
> A few small issues you might consider addressing in follow-ups.

It also seems there are some format warnings on x86, see

https://ci.appveyor.com/project/cygwin/cygwin/builds/40046785/job/fie6x4ta11v5nrjo

^ permalink raw reply	[flat|nested] 9+ messages in thread

* [PATCH] Cygwin: fix format warnings in profiler.cc
  2021-07-19 15:43     ` Jon Turney
@ 2021-07-21  8:00       ` Mark Geisert
  2021-07-21  8:09         ` Corinna Vinschen
  2021-07-21  8:07       ` [PATCH 1/3] Cygwin: New tool: profiler Corinna Vinschen
  1 sibling, 1 reply; 9+ messages in thread
From: Mark Geisert @ 2021-07-21  8:00 UTC (permalink / raw)
  To: cygwin-patches

Use new typedef to normalize pids for printing on both 32- and 64-bit Cygwin.

---
 winsup/utils/profiler.cc | 9 +++++----
 1 file changed, 5 insertions(+), 4 deletions(-)

diff --git a/winsup/utils/profiler.cc b/winsup/utils/profiler.cc
index d1a01c3a2..152bf1cca 100644
--- a/winsup/utils/profiler.cc
+++ b/winsup/utils/profiler.cc
@@ -29,6 +29,7 @@
 #include "cygwin/version.h"
 #include "cygtls_padsize.h"
 #include "gcc_seh.h"
+typedef unsigned long ulong;
 typedef unsigned short ushort;
 typedef uint16_t u_int16_t; // Non-standard sized type needed by ancient gmon.h
 #define NO_GLOBALS_H
@@ -312,10 +313,10 @@ dump_profile_data (child *c)
       if (s->name)
         {
           WCHAR *name = 1 + wcsrchr (s->name, L'\\');
-          sprintf (filename, "%s.%u.%ls", prefix, c->pid, name);
+          sprintf (filename, "%s.%lu.%ls", prefix, (ulong) c->pid, name);
         }
       else
-        sprintf (filename, "%s.%u", prefix, c->pid);
+        sprintf (filename, "%s.%lu", prefix, (ulong) c->pid);
 
       fd = open (filename, O_CREAT | O_TRUNC | O_WRONLY | O_BINARY);
       if (fd < 0)
@@ -804,9 +805,9 @@ cygwin_pid (DWORD winpid)
   cygpid = (DWORD) cygwin_internal (CW_WINPID_TO_CYGWIN_PID, winpid);
 
   if (cygpid >= max_cygpid)
-    snprintf (buf, sizeof buf, "%u", winpid);
+    snprintf (buf, sizeof buf, "%lu", (ulong) winpid);
   else
-    snprintf (buf, sizeof buf, "%u (pid: %u)", winpid, cygpid);
+    snprintf (buf, sizeof buf, "%lu (pid: %lu)", (ulong) winpid, (ulong) cygpid);
   return buf;
 }
 
-- 
2.32.0


^ permalink raw reply	[flat|nested] 9+ messages in thread

* Re: [PATCH 1/3] Cygwin: New tool: profiler
  2021-07-19 15:43     ` Jon Turney
  2021-07-21  8:00       ` [PATCH] Cygwin: fix format warnings in profiler.cc Mark Geisert
@ 2021-07-21  8:07       ` Corinna Vinschen
  1 sibling, 0 replies; 9+ messages in thread
From: Corinna Vinschen @ 2021-07-21  8:07 UTC (permalink / raw)
  To: cygwin-patches

On Jul 19 16:43, Jon Turney wrote:
> On 19/07/2021 15:23, Jon Turney wrote:
> > On 19/07/2021 11:04, Corinna Vinschen wrote:
> > > Hi Matt,
> > > 
> > > On Jul 15 21:49, Mark Geisert wrote:
> > > > The new tool formerly known as cygmon is renamed to 'profiler'.  For the
> > > > name I considered 'ipsampler' and could not think of any
> > > > others.  I'm open
> > > > to a different name if any is suggested.
> > > > 
> > > > I decided that a discussion of the pros and cons of this profiler vs the
> > > > existing ssp should probably be in the "Profiling Cygwin
> > > > Programs" section
> > > > of the Cygwin User's Guide rather than in the help for either.  That
> > > > material will be supplied at some point.
> > > > 
> > > > CONTEXT buffers are made child-specific and thus thread-specific since
> > > > there is one profiler thread for each child program being profiled.
> > > > 
> > > > The SetThreadPriority() warning comment has been expanded.
> > > > 
> > > > chmod() works on Cygwin so the "//XXX ineffective" comment is gone.
> > > > 
> > > > I decided to make the "sample all executable sections" and "sample
> > > > dynamically generated code" suggestions simply expanded comments
> > > > for now.
> > > > 
> > > > The profiler program is now a Cygwin exe rather than a native exe.
> > > 
> > > The patchset LGTM, but for the details I'd like jturney to have a look
> > > and approve it eventually.
> > 
> > Thanks.  I applied these patches.
> > 
> > A few small issues you might consider addressing in follow-ups.
> 
> It also seems there are some format warnings on x86, see
> 
> https://ci.appveyor.com/project/cygwin/cygwin/builds/40046785/job/fie6x4ta11v5nrjo

Given that we build with -Werror, these warnings are fatal.  I pushed
a patch to fix this.


Corinna

^ permalink raw reply	[flat|nested] 9+ messages in thread

* Re: [PATCH] Cygwin: fix format warnings in profiler.cc
  2021-07-21  8:00       ` [PATCH] Cygwin: fix format warnings in profiler.cc Mark Geisert
@ 2021-07-21  8:09         ` Corinna Vinschen
  0 siblings, 0 replies; 9+ messages in thread
From: Corinna Vinschen @ 2021-07-21  8:09 UTC (permalink / raw)
  To: cygwin-patches

On Jul 21 01:00, Mark Geisert wrote:
> Use new typedef to normalize pids for printing on both 32- and 64-bit Cygwin.

I pushed my patch before I saw yours.  If you want to use ulong, please
send a followup patch which introduces the typedef.


Thanks,
Corinna

^ permalink raw reply	[flat|nested] 9+ messages in thread

end of thread, other threads:[~2021-07-21  8:09 UTC | newest]

Thread overview: 9+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2021-07-16  4:49 [PATCH 1/3] Cygwin: New tool: profiler Mark Geisert
2021-07-16  4:49 ` [PATCH 2/3] Cygwin: New tool: gmondump Mark Geisert
2021-07-16  4:49 ` [PATCH 3/3] Cygwin: updates to wire in profiler, gmondump Mark Geisert
2021-07-19 10:04 ` [PATCH 1/3] Cygwin: New tool: profiler Corinna Vinschen
2021-07-19 14:23   ` Jon Turney
2021-07-19 15:43     ` Jon Turney
2021-07-21  8:00       ` [PATCH] Cygwin: fix format warnings in profiler.cc Mark Geisert
2021-07-21  8:09         ` Corinna Vinschen
2021-07-21  8:07       ` [PATCH 1/3] Cygwin: New tool: profiler Corinna Vinschen

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).