Process Hacker
gpusys.c
Go to the documentation of this file.
1 /*
2  * Process Hacker Extended Tools -
3  * GPU system information section
4  *
5  * Copyright (C) 2011 wj32
6  *
7  * This file is part of Process Hacker.
8  *
9  * Process Hacker is free software; you can redistribute it and/or modify
10  * it under the terms of the GNU General Public License as published by
11  * the Free Software Foundation, either version 3 of the License, or
12  * (at your option) any later version.
13  *
14  * Process Hacker is distributed in the hope that it will be useful,
15  * but WITHOUT ANY WARRANTY; without even the implied warranty of
16  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17  * GNU General Public License for more details.
18  *
19  * You should have received a copy of the GNU General Public License
20  * along with Process Hacker. If not, see <http://www.gnu.org/licenses/>.
21  */
22 
23 #include "exttools.h"
24 #include "resource.h"
25 #include <windowsx.h>
26 #include "gpusys.h"
27 
28 static PPH_SYSINFO_SECTION GpuSection;
29 static HWND GpuDialog;
30 static PH_LAYOUT_MANAGER GpuLayoutManager;
31 static RECT GpuGraphMargin;
32 static HWND GpuGraphHandle;
33 static PH_GRAPH_STATE GpuGraphState;
34 static HWND DedicatedGraphHandle;
35 static PH_GRAPH_STATE DedicatedGraphState;
36 static HWND SharedGraphHandle;
37 static PH_GRAPH_STATE SharedGraphState;
38 static HWND GpuPanel;
39 
41  _In_ PPH_PLUGIN_SYSINFO_POINTERS Pointers
42  )
43 {
44  PH_SYSINFO_SECTION section;
45 
46  memset(&section, 0, sizeof(PH_SYSINFO_SECTION));
47  PhInitializeStringRef(&section.Name, L"GPU");
48  section.Flags = 0;
50 
51  GpuSection = Pointers->CreateSection(&section);
52 }
53 
55  _In_ PPH_SYSINFO_SECTION Section,
56  _In_ PH_SYSINFO_SECTION_MESSAGE Message,
57  _In_opt_ PVOID Parameter1,
58  _In_opt_ PVOID Parameter2
59  )
60 {
61  switch (Message)
62  {
63  case SysInfoDestroy:
64  {
65  if (GpuDialog)
66  {
68  GpuDialog = NULL;
69  }
70  }
71  return TRUE;
72  case SysInfoTick:
73  {
74  if (GpuDialog)
75  {
77  }
78  }
79  return TRUE;
81  {
82  PPH_SYSINFO_CREATE_DIALOG createDialog = Parameter1;
83 
84  createDialog->Instance = PluginInstance->DllBase;
85  createDialog->Template = MAKEINTRESOURCE(IDD_SYSINFO_GPU);
86  createDialog->DialogProc = EtpGpuDialogProc;
87  }
88  return TRUE;
90  {
91  PPH_GRAPH_DRAW_INFO drawInfo = Parameter1;
92 
93  drawInfo->Flags = PH_GRAPH_USE_GRID;
94  Section->Parameters->ColorSetupFunction(drawInfo, PhGetIntegerSetting(L"ColorCpuKernel"), 0);
95  PhGetDrawInfoGraphBuffers(&Section->GraphState.Buffers, drawInfo, EtGpuNodeHistory.Count);
96 
97  if (!Section->GraphState.Valid)
98  {
99  PhCopyCircularBuffer_FLOAT(&EtGpuNodeHistory, Section->GraphState.Data1, drawInfo->LineDataCount);
100  Section->GraphState.Valid = TRUE;
101  }
102  }
103  return TRUE;
105  {
106  PPH_SYSINFO_GRAPH_GET_TOOLTIP_TEXT getTooltipText = Parameter1;
107  FLOAT gpu;
108 
109  gpu = PhGetItemCircularBuffer_FLOAT(&EtGpuNodeHistory, getTooltipText->Index);
110 
111  PhMoveReference(&Section->GraphState.TooltipText, PhFormatString(
112  L"%.2f%%%s\n%s",
113  gpu * 100,
114  PhGetStringOrEmpty(EtpGetMaxNodeString(getTooltipText->Index)),
115  ((PPH_STRING)PhAutoDereferenceObject(PhGetStatisticsTimeString(NULL, getTooltipText->Index)))->Buffer
116  ));
117  getTooltipText->Text = Section->GraphState.TooltipText->sr;
118  }
119  return TRUE;
121  {
122  PPH_SYSINFO_DRAW_PANEL drawPanel = Parameter1;
123 
124  drawPanel->Title = PhCreateString(L"GPU");
125  drawPanel->SubTitle = PhFormatString(L"%.2f%%", EtGpuNodeUsage * 100);
126  }
127  return TRUE;
128  }
129 
130  return FALSE;
131 }
132 
134  VOID
135  )
136 {
137  PhInitializeGraphState(&GpuGraphState);
138  PhInitializeGraphState(&DedicatedGraphState);
139  PhInitializeGraphState(&SharedGraphState);
140 }
141 
143  VOID
144  )
145 {
146  PhDeleteGraphState(&GpuGraphState);
147  PhDeleteGraphState(&DedicatedGraphState);
148  PhDeleteGraphState(&SharedGraphState);
149 }
150 
152  VOID
153  )
154 {
157 }
158 
159 INT_PTR CALLBACK EtpGpuDialogProc(
160  _In_ HWND hwndDlg,
161  _In_ UINT uMsg,
162  _In_ WPARAM wParam,
163  _In_ LPARAM lParam
164  )
165 {
166  switch (uMsg)
167  {
168  case WM_INITDIALOG:
169  {
170  PPH_LAYOUT_ITEM graphItem;
171  PPH_LAYOUT_ITEM panelItem;
172 
174 
175  GpuDialog = hwndDlg;
176  PhInitializeLayoutManager(&GpuLayoutManager, hwndDlg);
177  PhAddLayoutItem(&GpuLayoutManager, GetDlgItem(hwndDlg, IDC_GPUNAME), NULL, PH_ANCHOR_LEFT | PH_ANCHOR_TOP | PH_ANCHOR_RIGHT | PH_LAYOUT_FORCE_INVALIDATE);
178  graphItem = PhAddLayoutItem(&GpuLayoutManager, GetDlgItem(hwndDlg, IDC_GRAPH_LAYOUT), NULL, PH_ANCHOR_ALL);
179  GpuGraphMargin = graphItem->Margin;
180  panelItem = PhAddLayoutItem(&GpuLayoutManager, GetDlgItem(hwndDlg, IDC_PANEL_LAYOUT), NULL, PH_ANCHOR_LEFT | PH_ANCHOR_RIGHT | PH_ANCHOR_BOTTOM);
181 
182  SendMessage(GetDlgItem(hwndDlg, IDC_TITLE), WM_SETFONT, (WPARAM)GpuSection->Parameters->LargeFont, FALSE);
183  SendMessage(GetDlgItem(hwndDlg, IDC_GPUNAME), WM_SETFONT, (WPARAM)GpuSection->Parameters->MediumFont, FALSE);
184 
185  SetDlgItemText(hwndDlg, IDC_GPUNAME, ((PPH_STRING)PhAutoDereferenceObject(EtpGetGpuNameString()))->Buffer);
186 
187  GpuPanel = CreateDialog(PluginInstance->DllBase, MAKEINTRESOURCE(IDD_SYSINFO_GPUPANEL), hwndDlg, EtpGpuPanelDialogProc);
188  ShowWindow(GpuPanel, SW_SHOW);
189  PhAddLayoutItemEx(&GpuLayoutManager, GpuPanel, NULL, PH_ANCHOR_LEFT | PH_ANCHOR_RIGHT | PH_ANCHOR_BOTTOM, panelItem->Margin);
190 
194  }
195  break;
196  case WM_DESTROY:
197  {
198  PhDeleteLayoutManager(&GpuLayoutManager);
199  }
200  break;
201  case WM_SIZE:
202  {
203  PhLayoutManagerLayout(&GpuLayoutManager);
205  }
206  break;
207  case WM_NOTIFY:
208  {
209  NMHDR *header = (NMHDR *)lParam;
210 
211  if (header->hwndFrom == GpuGraphHandle)
212  {
213  EtpNotifyGpuGraph(header);
214  }
215  else if (header->hwndFrom == DedicatedGraphHandle)
216  {
217  EtpNotifyDedicatedGraph(header);
218  }
219  else if (header->hwndFrom == SharedGraphHandle)
220  {
221  EtpNotifySharedGraph(header);
222  }
223  }
224  break;
225  }
226 
227  return FALSE;
228 }
229 
230 INT_PTR CALLBACK EtpGpuPanelDialogProc(
231  _In_ HWND hwndDlg,
232  _In_ UINT uMsg,
233  _In_ WPARAM wParam,
234  _In_ LPARAM lParam
235  )
236 {
237  switch (uMsg)
238  {
239  case WM_COMMAND:
240  {
241  switch (LOWORD(wParam))
242  {
243  case IDC_NODES:
244  EtShowGpuNodesDialog(GpuDialog, GpuSection->Parameters);
245  break;
246  }
247  }
248  break;
249  }
250 
251  return FALSE;
252 }
253 
255  VOID
256  )
257 {
258  GpuGraphHandle = CreateWindow(
260  NULL,
261  WS_VISIBLE | WS_CHILD | WS_BORDER,
262  0,
263  0,
264  3,
265  3,
266  GpuDialog,
267  NULL,
268  NULL,
269  NULL
270  );
271  Graph_SetTooltip(GpuGraphHandle, TRUE);
272 
273  DedicatedGraphHandle = CreateWindow(
275  NULL,
276  WS_VISIBLE | WS_CHILD | WS_BORDER,
277  0,
278  0,
279  3,
280  3,
281  GpuDialog,
282  NULL,
283  NULL,
284  NULL
285  );
286  Graph_SetTooltip(DedicatedGraphHandle, TRUE);
287 
288  SharedGraphHandle = CreateWindow(
290  NULL,
291  WS_VISIBLE | WS_CHILD | WS_BORDER,
292  0,
293  0,
294  3,
295  3,
296  GpuDialog,
297  NULL,
298  NULL,
299  NULL
300  );
301  Graph_SetTooltip(SharedGraphHandle, TRUE);
302 }
303 
305  VOID
306  )
307 {
308  RECT clientRect;
309  RECT labelRect;
310  ULONG graphWidth;
311  ULONG graphHeight;
312  HDWP deferHandle;
313  ULONG y;
314 
315  GetClientRect(GpuDialog, &clientRect);
316  GetClientRect(GetDlgItem(GpuDialog, IDC_GPU_L), &labelRect);
317  graphWidth = clientRect.right - GpuGraphMargin.left - GpuGraphMargin.right;
318  graphHeight = (clientRect.bottom - GpuGraphMargin.top - GpuGraphMargin.bottom - labelRect.bottom * 3 - ET_GPU_PADDING * 5) / 3;
319 
320  deferHandle = BeginDeferWindowPos(6);
321  y = GpuGraphMargin.top;
322 
323  deferHandle = DeferWindowPos(
324  deferHandle,
325  GetDlgItem(GpuDialog, IDC_GPU_L),
326  NULL,
327  GpuGraphMargin.left,
328  y,
329  0,
330  0,
331  SWP_NOSIZE | SWP_NOACTIVATE | SWP_NOZORDER
332  );
333  y += labelRect.bottom + ET_GPU_PADDING;
334 
335  deferHandle = DeferWindowPos(
336  deferHandle,
337  GpuGraphHandle,
338  NULL,
339  GpuGraphMargin.left,
340  y,
341  graphWidth,
342  graphHeight,
343  SWP_NOACTIVATE | SWP_NOZORDER
344  );
345  y += graphHeight + ET_GPU_PADDING;
346 
347  deferHandle = DeferWindowPos(
348  deferHandle,
349  GetDlgItem(GpuDialog, IDC_DEDICATED_L),
350  NULL,
351  GpuGraphMargin.left,
352  y,
353  0,
354  0,
355  SWP_NOSIZE | SWP_NOACTIVATE | SWP_NOZORDER
356  );
357  y += labelRect.bottom + ET_GPU_PADDING;
358 
359  deferHandle = DeferWindowPos(
360  deferHandle,
361  DedicatedGraphHandle,
362  NULL,
363  GpuGraphMargin.left,
364  y,
365  graphWidth,
366  graphHeight,
367  SWP_NOACTIVATE | SWP_NOZORDER
368  );
369  y += graphHeight + ET_GPU_PADDING;
370 
371  deferHandle = DeferWindowPos(
372  deferHandle,
373  GetDlgItem(GpuDialog, IDC_SHARED_L),
374  NULL,
375  GpuGraphMargin.left,
376  y,
377  0,
378  0,
379  SWP_NOSIZE | SWP_NOACTIVATE | SWP_NOZORDER
380  );
381  y += labelRect.bottom + ET_GPU_PADDING;
382 
383  deferHandle = DeferWindowPos(
384  deferHandle,
385  SharedGraphHandle,
386  NULL,
387  GpuGraphMargin.left,
388  y,
389  graphWidth,
390  clientRect.bottom - GpuGraphMargin.bottom - y,
391  SWP_NOACTIVATE | SWP_NOZORDER
392  );
393 
394  EndDeferWindowPos(deferHandle);
395 }
396 
398  _In_ NMHDR *Header
399  )
400 {
401  switch (Header->code)
402  {
403  case GCN_GETDRAWINFO:
404  {
405  PPH_GRAPH_GETDRAWINFO getDrawInfo = (PPH_GRAPH_GETDRAWINFO)Header;
406  PPH_GRAPH_DRAW_INFO drawInfo = getDrawInfo->DrawInfo;
407 
408  drawInfo->Flags = PH_GRAPH_USE_GRID;
409  GpuSection->Parameters->ColorSetupFunction(drawInfo, PhGetIntegerSetting(L"ColorCpuKernel"), 0);
410 
412  &GpuGraphState,
413  getDrawInfo,
414  EtGpuNodeHistory.Count
415  );
416 
417  if (!GpuGraphState.Valid)
418  {
419  PhCopyCircularBuffer_FLOAT(&EtGpuNodeHistory, GpuGraphState.Data1, drawInfo->LineDataCount);
420  GpuGraphState.Valid = TRUE;
421  }
422  }
423  break;
424  case GCN_GETTOOLTIPTEXT:
425  {
426  PPH_GRAPH_GETTOOLTIPTEXT getTooltipText = (PPH_GRAPH_GETTOOLTIPTEXT)Header;
427 
428  if (getTooltipText->Index < getTooltipText->TotalCount)
429  {
430  if (GpuGraphState.TooltipIndex != getTooltipText->Index)
431  {
432  FLOAT gpu;
433 
434  gpu = PhGetItemCircularBuffer_FLOAT(&EtGpuNodeHistory, getTooltipText->Index);
435 
437  L"%.2f%%%s\n%s",
438  gpu * 100,
439  PhGetStringOrEmpty(EtpGetMaxNodeString(getTooltipText->Index)),
440  ((PPH_STRING)PhAutoDereferenceObject(PhGetStatisticsTimeString(NULL, getTooltipText->Index)))->Buffer
441  ));
442  }
443 
444  getTooltipText->Text = GpuGraphState.TooltipText->sr;
445  }
446  }
447  break;
448  case GCN_MOUSEEVENT:
449  {
450  PPH_GRAPH_MOUSEEVENT mouseEvent = (PPH_GRAPH_MOUSEEVENT)Header;
451  PPH_PROCESS_RECORD record;
452 
453  record = NULL;
454 
455  if (mouseEvent->Message == WM_LBUTTONDBLCLK && mouseEvent->Index < mouseEvent->TotalCount)
456  {
457  record = EtpReferenceMaxNodeRecord(mouseEvent->Index);
458  }
459 
460  if (record)
461  {
462  PhShowProcessRecordDialog(GpuDialog, record);
464  }
465  }
466  break;
467  }
468 }
469 
471  _In_ NMHDR *Header
472  )
473 {
474  switch (Header->code)
475  {
476  case GCN_GETDRAWINFO:
477  {
478  PPH_GRAPH_GETDRAWINFO getDrawInfo = (PPH_GRAPH_GETDRAWINFO)Header;
479  PPH_GRAPH_DRAW_INFO drawInfo = getDrawInfo->DrawInfo;
480  ULONG i;
481 
482  drawInfo->Flags = PH_GRAPH_USE_GRID;
483  GpuSection->Parameters->ColorSetupFunction(drawInfo, PhGetIntegerSetting(L"ColorPrivate"), 0);
484 
486  &DedicatedGraphState,
487  getDrawInfo,
489  );
490 
491  if (!DedicatedGraphState.Valid)
492  {
493  for (i = 0; i < drawInfo->LineDataCount; i++)
494  {
495  DedicatedGraphState.Data1[i] =
496  (FLOAT)PhGetItemCircularBuffer_ULONG(&EtGpuDedicatedHistory, i);
497  }
498 
499  if (EtGpuDedicatedLimit != 0)
500  {
501  // Scale the data.
503  DedicatedGraphState.Data1,
505  drawInfo->LineDataCount
506  );
507  }
508 
509  DedicatedGraphState.Valid = TRUE;
510  }
511  }
512  break;
513  case GCN_GETTOOLTIPTEXT:
514  {
515  PPH_GRAPH_GETTOOLTIPTEXT getTooltipText = (PPH_GRAPH_GETTOOLTIPTEXT)Header;
516 
517  if (getTooltipText->Index < getTooltipText->TotalCount)
518  {
519  if (DedicatedGraphState.TooltipIndex != getTooltipText->Index)
520  {
521  ULONG usedPages;
522 
523  usedPages = PhGetItemCircularBuffer_ULONG(&EtGpuDedicatedHistory, getTooltipText->Index);
524 
525  PhMoveReference(&DedicatedGraphState.TooltipText, PhFormatString(
526  L"Dedicated Memory: %s\n%s",
527  PhaFormatSize(UInt32x32To64(usedPages, PAGE_SIZE), -1)->Buffer,
528  ((PPH_STRING)PhAutoDereferenceObject(PhGetStatisticsTimeString(NULL, getTooltipText->Index)))->Buffer
529  ));
530  }
531 
532  getTooltipText->Text = DedicatedGraphState.TooltipText->sr;
533  }
534  }
535  break;
536  }
537 }
538 
540  _In_ NMHDR *Header
541  )
542 {
543  switch (Header->code)
544  {
545  case GCN_GETDRAWINFO:
546  {
547  PPH_GRAPH_GETDRAWINFO getDrawInfo = (PPH_GRAPH_GETDRAWINFO)Header;
548  PPH_GRAPH_DRAW_INFO drawInfo = getDrawInfo->DrawInfo;
549  ULONG i;
550 
551  drawInfo->Flags = PH_GRAPH_USE_GRID;
552  GpuSection->Parameters->ColorSetupFunction(drawInfo, PhGetIntegerSetting(L"ColorPhysical"), 0);
553 
555  &SharedGraphState,
556  getDrawInfo,
557  EtGpuSharedHistory.Count
558  );
559 
560  if (!SharedGraphState.Valid)
561  {
562  for (i = 0; i < drawInfo->LineDataCount; i++)
563  {
564  SharedGraphState.Data1[i] =
565  (FLOAT)PhGetItemCircularBuffer_ULONG(&EtGpuSharedHistory, i);
566  }
567 
568  if (EtGpuSharedLimit != 0)
569  {
570  // Scale the data.
572  SharedGraphState.Data1,
573  (FLOAT)EtGpuSharedLimit / PAGE_SIZE,
574  drawInfo->LineDataCount
575  );
576  }
577 
578  SharedGraphState.Valid = TRUE;
579  }
580  }
581  break;
582  case GCN_GETTOOLTIPTEXT:
583  {
584  PPH_GRAPH_GETTOOLTIPTEXT getTooltipText = (PPH_GRAPH_GETTOOLTIPTEXT)Header;
585 
586  if (getTooltipText->Index < getTooltipText->TotalCount)
587  {
588  if (SharedGraphState.TooltipIndex != getTooltipText->Index)
589  {
590  ULONG usedPages;
591 
592  usedPages = PhGetItemCircularBuffer_ULONG(&EtGpuSharedHistory, getTooltipText->Index);
593 
594  PhMoveReference(&SharedGraphState.TooltipText, PhFormatString(
595  L"Shared Memory: %s\n%s",
596  PhaFormatSize(UInt32x32To64(usedPages, PAGE_SIZE), -1)->Buffer,
597  ((PPH_STRING)PhAutoDereferenceObject(PhGetStatisticsTimeString(NULL, getTooltipText->Index)))->Buffer
598  ));
599  }
600 
601  getTooltipText->Text = SharedGraphState.TooltipText->sr;
602  }
603  }
604  break;
605  }
606 }
607 
609  VOID
610  )
611 {
612  GpuGraphState.Valid = FALSE;
613  GpuGraphState.TooltipIndex = -1;
614  Graph_MoveGrid(GpuGraphHandle, 1);
615  Graph_Draw(GpuGraphHandle);
616  Graph_UpdateTooltip(GpuGraphHandle);
617  InvalidateRect(GpuGraphHandle, NULL, FALSE);
618 
619  DedicatedGraphState.Valid = FALSE;
620  DedicatedGraphState.TooltipIndex = -1;
621  Graph_MoveGrid(DedicatedGraphHandle, 1);
622  Graph_Draw(DedicatedGraphHandle);
623  Graph_UpdateTooltip(DedicatedGraphHandle);
624  InvalidateRect(DedicatedGraphHandle, NULL, FALSE);
625 
626  SharedGraphState.Valid = FALSE;
627  SharedGraphState.TooltipIndex = -1;
628  Graph_MoveGrid(SharedGraphHandle, 1);
629  Graph_Draw(SharedGraphHandle);
630  Graph_UpdateTooltip(SharedGraphHandle);
631  InvalidateRect(SharedGraphHandle, NULL, FALSE);
632 }
633 
635  VOID
636  )
637 {
638  SetDlgItemText(GpuPanel, IDC_ZDEDICATEDCURRENT_V, PhaFormatSize(EtGpuDedicatedUsage, -1)->Buffer);
639  SetDlgItemText(GpuPanel, IDC_ZDEDICATEDLIMIT_V, PhaFormatSize(EtGpuDedicatedLimit, -1)->Buffer);
640 
641  SetDlgItemText(GpuPanel, IDC_ZSHAREDCURRENT_V, PhaFormatSize(EtGpuSharedUsage, -1)->Buffer);
642  SetDlgItemText(GpuPanel, IDC_ZSHAREDLIMIT_V, PhaFormatSize(EtGpuSharedLimit, -1)->Buffer);
643 }
644 
646  _In_ LONG Index
647  )
648 {
649  LARGE_INTEGER time;
650  ULONG maxProcessId;
651 
652  maxProcessId = PhGetItemCircularBuffer_ULONG(&EtMaxGpuNodeHistory, Index);
653 
654  if (!maxProcessId)
655  return NULL;
656 
657  PhGetStatisticsTime(NULL, Index, &time);
658  time.QuadPart += PH_TICKS_PER_SEC - 1;
659 
660  return PhFindProcessRecord(UlongToHandle(maxProcessId), &time);
661 }
662 
664  _In_ LONG Index
665  )
666 {
667  PPH_PROCESS_RECORD maxProcessRecord;
668  FLOAT maxGpuUsage;
669  PPH_STRING maxUsageString = NULL;
670 
671  if (maxProcessRecord = EtpReferenceMaxNodeRecord(Index))
672  {
673  maxGpuUsage = PhGetItemCircularBuffer_FLOAT(&EtMaxGpuNodeUsageHistory, Index);
674 
675  maxUsageString = PhaFormatString(
676  L"\n%s (%lu): %.2f%%",
677  maxProcessRecord->ProcessName->Buffer,
678  HandleToUlong(maxProcessRecord->ProcessId),
679  maxGpuUsage * 100
680  );
681 
682  PhDereferenceProcessRecord(maxProcessRecord);
683  }
684 
685  return maxUsageString;
686 }
687 
689  VOID
690  )
691 {
692  ULONG i;
693  ULONG count;
695 
696  count = EtGetGpuAdapterCount();
697  PhInitializeStringBuilder(&sb, 100);
698 
699  for (i = 0; i < count; i++)
700  {
701  PPH_STRING description;
702 
703  description = EtGetGpuAdapterDescription(i);
704 
705  if (!PhIsNullOrEmptyString(description))
706  {
707  // Ignore "Microsoft Basic Render Driver" unless we don't have any other adapters.
708  // This does not take into account localization.
709  if (count == 1 || !PhEqualString2(description, L"Microsoft Basic Render Driver", TRUE))
710  {
711  PhAppendStringBuilder(&sb, &description->sr);
712  PhAppendStringBuilder2(&sb, L", ");
713  }
714  }
715 
716  if (description)
717  PhDereferenceObject(description);
718  }
719 
720  if (sb.String->Length != 0)
721  PhRemoveEndStringBuilder(&sb, 2);
722 
723  return PhFinalStringBuilderString(&sb);
724 }