This commit is contained in:
lifestorm
2024-08-05 18:40:29 +03:00
parent 9f505a0646
commit c6d9b6f580
8044 changed files with 1853472 additions and 21 deletions

502
addons/fprofiler/LICENSE Normal file
View File

@@ -0,0 +1,502 @@
GNU LESSER GENERAL PUBLIC LICENSE
Version 2.1, February 1999
Copyright (C) 1991, 1999 Free Software Foundation, Inc.
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
[This is the first released version of the Lesser GPL. It also counts
as the successor of the GNU Library Public License, version 2, hence
the version number 2.1.]
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
Licenses are intended to guarantee your freedom to share and change
free software--to make sure the software is free for all its users.
This license, the Lesser General Public License, applies to some
specially designated software packages--typically libraries--of the
Free Software Foundation and other authors who decide to use it. You
can use it too, but we suggest you first think carefully about whether
this license or the ordinary General Public License is the better
strategy to use in any particular case, based on the explanations below.
When we speak of free software, we are referring to freedom of use,
not price. Our General Public Licenses are designed to make sure that
you have the freedom to distribute copies of free software (and charge
for this service if you wish); that you receive source code or can get
it if you want it; that you can change the software and use pieces of
it in new free programs; and that you are informed that you can do
these things.
To protect your rights, we need to make restrictions that forbid
distributors to deny you these rights or to ask you to surrender these
rights. These restrictions translate to certain responsibilities for
you if you distribute copies of the library or if you modify it.
For example, if you distribute copies of the library, whether gratis
or for a fee, you must give the recipients all the rights that we gave
you. You must make sure that they, too, receive or can get the source
code. If you link other code with the library, you must provide
complete object files to the recipients, so that they can relink them
with the library after making changes to the library and recompiling
it. And you must show them these terms so they know their rights.
We protect your rights with a two-step method: (1) we copyright the
library, and (2) we offer you this license, which gives you legal
permission to copy, distribute and/or modify the library.
To protect each distributor, we want to make it very clear that
there is no warranty for the free library. Also, if the library is
modified by someone else and passed on, the recipients should know
that what they have is not the original version, so that the original
author's reputation will not be affected by problems that might be
introduced by others.
Finally, software patents pose a constant threat to the existence of
any free program. We wish to make sure that a company cannot
effectively restrict the users of a free program by obtaining a
restrictive license from a patent holder. Therefore, we insist that
any patent license obtained for a version of the library must be
consistent with the full freedom of use specified in this license.
Most GNU software, including some libraries, is covered by the
ordinary GNU General Public License. This license, the GNU Lesser
General Public License, applies to certain designated libraries, and
is quite different from the ordinary General Public License. We use
this license for certain libraries in order to permit linking those
libraries into non-free programs.
When a program is linked with a library, whether statically or using
a shared library, the combination of the two is legally speaking a
combined work, a derivative of the original library. The ordinary
General Public License therefore permits such linking only if the
entire combination fits its criteria of freedom. The Lesser General
Public License permits more lax criteria for linking other code with
the library.
We call this license the "Lesser" General Public License because it
does Less to protect the user's freedom than the ordinary General
Public License. It also provides other free software developers Less
of an advantage over competing non-free programs. These disadvantages
are the reason we use the ordinary General Public License for many
libraries. However, the Lesser license provides advantages in certain
special circumstances.
For example, on rare occasions, there may be a special need to
encourage the widest possible use of a certain library, so that it becomes
a de-facto standard. To achieve this, non-free programs must be
allowed to use the library. A more frequent case is that a free
library does the same job as widely used non-free libraries. In this
case, there is little to gain by limiting the free library to free
software only, so we use the Lesser General Public License.
In other cases, permission to use a particular library in non-free
programs enables a greater number of people to use a large body of
free software. For example, permission to use the GNU C Library in
non-free programs enables many more people to use the whole GNU
operating system, as well as its variant, the GNU/Linux operating
system.
Although the Lesser General Public License is Less protective of the
users' freedom, it does ensure that the user of a program that is
linked with the Library has the freedom and the wherewithal to run
that program using a modified version of the Library.
The precise terms and conditions for copying, distribution and
modification follow. Pay close attention to the difference between a
"work based on the library" and a "work that uses the library". The
former contains code derived from the library, whereas the latter must
be combined with the library in order to run.
GNU LESSER GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License Agreement applies to any software library or other
program which contains a notice placed by the copyright holder or
other authorized party saying it may be distributed under the terms of
this Lesser General Public License (also called "this License").
Each licensee is addressed as "you".
A "library" means a collection of software functions and/or data
prepared so as to be conveniently linked with application programs
(which use some of those functions and data) to form executables.
The "Library", below, refers to any such software library or work
which has been distributed under these terms. A "work based on the
Library" means either the Library or any derivative work under
copyright law: that is to say, a work containing the Library or a
portion of it, either verbatim or with modifications and/or translated
straightforwardly into another language. (Hereinafter, translation is
included without limitation in the term "modification".)
"Source code" for a work means the preferred form of the work for
making modifications to it. For a library, complete source code means
all the source code for all modules it contains, plus any associated
interface definition files, plus the scripts used to control compilation
and installation of the library.
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running a program using the Library is not restricted, and output from
such a program is covered only if its contents constitute a work based
on the Library (independent of the use of the Library in a tool for
writing it). Whether that is true depends on what the Library does
and what the program that uses the Library does.
1. You may copy and distribute verbatim copies of the Library's
complete source code as you receive it, in any medium, provided that
you conspicuously and appropriately publish on each copy an
appropriate copyright notice and disclaimer of warranty; keep intact
all the notices that refer to this License and to the absence of any
warranty; and distribute a copy of this License along with the
Library.
You may charge a fee for the physical act of transferring a copy,
and you may at your option offer warranty protection in exchange for a
fee.
2. You may modify your copy or copies of the Library or any portion
of it, thus forming a work based on the Library, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) The modified work must itself be a software library.
b) You must cause the files modified to carry prominent notices
stating that you changed the files and the date of any change.
c) You must cause the whole of the work to be licensed at no
charge to all third parties under the terms of this License.
d) If a facility in the modified Library refers to a function or a
table of data to be supplied by an application program that uses
the facility, other than as an argument passed when the facility
is invoked, then you must make a good faith effort to ensure that,
in the event an application does not supply such function or
table, the facility still operates, and performs whatever part of
its purpose remains meaningful.
(For example, a function in a library to compute square roots has
a purpose that is entirely well-defined independent of the
application. Therefore, Subsection 2d requires that any
application-supplied function or table used by this function must
be optional: if the application does not supply it, the square
root function must still compute square roots.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Library,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Library, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote
it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Library.
In addition, mere aggregation of another work not based on the Library
with the Library (or with a work based on the Library) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may opt to apply the terms of the ordinary GNU General Public
License instead of this License to a given copy of the Library. To do
this, you must alter all the notices that refer to this License, so
that they refer to the ordinary GNU General Public License, version 2,
instead of to this License. (If a newer version than version 2 of the
ordinary GNU General Public License has appeared, then you can specify
that version instead if you wish.) Do not make any other change in
these notices.
Once this change is made in a given copy, it is irreversible for
that copy, so the ordinary GNU General Public License applies to all
subsequent copies and derivative works made from that copy.
This option is useful when you wish to copy part of the code of
the Library into a program that is not a library.
4. You may copy and distribute the Library (or a portion or
derivative of it, under Section 2) in object code or executable form
under the terms of Sections 1 and 2 above provided that you accompany
it with the complete corresponding machine-readable source code, which
must be distributed under the terms of Sections 1 and 2 above on a
medium customarily used for software interchange.
If distribution of object code is made by offering access to copy
from a designated place, then offering equivalent access to copy the
source code from the same place satisfies the requirement to
distribute the source code, even though third parties are not
compelled to copy the source along with the object code.
5. A program that contains no derivative of any portion of the
Library, but is designed to work with the Library by being compiled or
linked with it, is called a "work that uses the Library". Such a
work, in isolation, is not a derivative work of the Library, and
therefore falls outside the scope of this License.
However, linking a "work that uses the Library" with the Library
creates an executable that is a derivative of the Library (because it
contains portions of the Library), rather than a "work that uses the
library". The executable is therefore covered by this License.
Section 6 states terms for distribution of such executables.
When a "work that uses the Library" uses material from a header file
that is part of the Library, the object code for the work may be a
derivative work of the Library even though the source code is not.
Whether this is true is especially significant if the work can be
linked without the Library, or if the work is itself a library. The
threshold for this to be true is not precisely defined by law.
If such an object file uses only numerical parameters, data
structure layouts and accessors, and small macros and small inline
functions (ten lines or less in length), then the use of the object
file is unrestricted, regardless of whether it is legally a derivative
work. (Executables containing this object code plus portions of the
Library will still fall under Section 6.)
Otherwise, if the work is a derivative of the Library, you may
distribute the object code for the work under the terms of Section 6.
Any executables containing that work also fall under Section 6,
whether or not they are linked directly with the Library itself.
6. As an exception to the Sections above, you may also combine or
link a "work that uses the Library" with the Library to produce a
work containing portions of the Library, and distribute that work
under terms of your choice, provided that the terms permit
modification of the work for the customer's own use and reverse
engineering for debugging such modifications.
You must give prominent notice with each copy of the work that the
Library is used in it and that the Library and its use are covered by
this License. You must supply a copy of this License. If the work
during execution displays copyright notices, you must include the
copyright notice for the Library among them, as well as a reference
directing the user to the copy of this License. Also, you must do one
of these things:
a) Accompany the work with the complete corresponding
machine-readable source code for the Library including whatever
changes were used in the work (which must be distributed under
Sections 1 and 2 above); and, if the work is an executable linked
with the Library, with the complete machine-readable "work that
uses the Library", as object code and/or source code, so that the
user can modify the Library and then relink to produce a modified
executable containing the modified Library. (It is understood
that the user who changes the contents of definitions files in the
Library will not necessarily be able to recompile the application
to use the modified definitions.)
b) Use a suitable shared library mechanism for linking with the
Library. A suitable mechanism is one that (1) uses at run time a
copy of the library already present on the user's computer system,
rather than copying library functions into the executable, and (2)
will operate properly with a modified version of the library, if
the user installs one, as long as the modified version is
interface-compatible with the version that the work was made with.
c) Accompany the work with a written offer, valid for at
least three years, to give the same user the materials
specified in Subsection 6a, above, for a charge no more
than the cost of performing this distribution.
d) If distribution of the work is made by offering access to copy
from a designated place, offer equivalent access to copy the above
specified materials from the same place.
e) Verify that the user has already received a copy of these
materials or that you have already sent this user a copy.
For an executable, the required form of the "work that uses the
Library" must include any data and utility programs needed for
reproducing the executable from it. However, as a special exception,
the materials to be distributed need not include anything that is
normally distributed (in either source or binary form) with the major
components (compiler, kernel, and so on) of the operating system on
which the executable runs, unless that component itself accompanies
the executable.
It may happen that this requirement contradicts the license
restrictions of other proprietary libraries that do not normally
accompany the operating system. Such a contradiction means you cannot
use both them and the Library together in an executable that you
distribute.
7. You may place library facilities that are a work based on the
Library side-by-side in a single library together with other library
facilities not covered by this License, and distribute such a combined
library, provided that the separate distribution of the work based on
the Library and of the other library facilities is otherwise
permitted, and provided that you do these two things:
a) Accompany the combined library with a copy of the same work
based on the Library, uncombined with any other library
facilities. This must be distributed under the terms of the
Sections above.
b) Give prominent notice with the combined library of the fact
that part of it is a work based on the Library, and explaining
where to find the accompanying uncombined form of the same work.
8. You may not copy, modify, sublicense, link with, or distribute
the Library except as expressly provided under this License. Any
attempt otherwise to copy, modify, sublicense, link with, or
distribute the Library is void, and will automatically terminate your
rights under this License. However, parties who have received copies,
or rights, from you under this License will not have their licenses
terminated so long as such parties remain in full compliance.
9. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Library or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Library (or any work based on the
Library), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Library or works based on it.
10. Each time you redistribute the Library (or any work based on the
Library), the recipient automatically receives a license from the
original licensor to copy, distribute, link with or modify the Library
subject to these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties with
this License.
11. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Library at all. For example, if a patent
license would not permit royalty-free redistribution of the Library by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Library.
If any portion of this section is held invalid or unenforceable under any
particular circumstance, the balance of the section is intended to apply,
and the section as a whole is intended to apply in other circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
12. If the distribution and/or use of the Library is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Library under this License may add
an explicit geographical distribution limitation excluding those countries,
so that distribution is permitted only in or among countries not thus
excluded. In such case, this License incorporates the limitation as if
written in the body of this License.
13. The Free Software Foundation may publish revised and/or new
versions of the Lesser General Public License from time to time.
Such new versions will be similar in spirit to the present version,
but may differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the Library
specifies a version number of this License which applies to it and
"any later version", you have the option of following the terms and
conditions either of that version or of any later version published by
the Free Software Foundation. If the Library does not specify a
license version number, you may choose any version ever published by
the Free Software Foundation.
14. If you wish to incorporate parts of the Library into other free
programs whose distribution conditions are incompatible with these,
write to the author to ask for permission. For software which is
copyrighted by the Free Software Foundation, write to the Free
Software Foundation; we sometimes make exceptions for this. Our
decision will be guided by the two goals of preserving the free status
of all derivatives of our free software and of promoting the sharing
and reuse of software generally.
NO WARRANTY
15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Libraries
If you develop a new library, and you want it to be of the greatest
possible use to the public, we recommend making it free software that
everyone can redistribute and change. You can do so by permitting
redistribution under these terms (or, alternatively, under the terms of the
ordinary General Public License).
To apply these terms, attach the following notices to the library. It is
safest to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least the
"copyright" line and a pointer to where the full notice is found.
<one line to give the library's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Also add information on how to contact you by electronic and paper mail.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the library, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the
library `Frob' (a library for tweaking knobs) written by James Random Hacker.
<signature of Ty Coon>, 1 April 1990
Ty Coon, President of Vice
That's all there is to it!

View File

@@ -0,0 +1,61 @@
# Garry's mod profiler
A profiler based on `debug.sethook` and `SysTime`. Perfect for the following things:
- Finding performance bottlenecks (i.e. which code is spent the most time on)
- Finding out which functions are called most
- Identifying the source of lag spikes
- Profiling specific functions
This profiler distinguishes itself from [DBugR](https://github.com/oubliette32/DBugR) in its approach.
DBugR profiles at hooks, timers and net messages specifically. FProfiler looks purely at functions outside of their context.
I would recommend DBugR to measure networking and/or specific hooks and/or timers. I would recommend FProfiler for the things mentioned above.
## Using FProfiler
The `FProfiler` console command opens the profiler. Everything can be done from there.
Here's an explanation of the FProfiler menu:
UI thing | Description
------------ | -------------
Realm | Whether you're profiling the client or the server. Note: You need to be a SuperAdmin (or have the `FProfiler` permission in your favourite admin mod) to be allowed to do any serverside profiling!
(Re)start profiling | Starts a profiling session. If there is any previous profiling session, it starts anew, disregarding any old data.
Stop profiling | Stop an ongoing profiling session.
Continue profiling | Continue a profiling session that has previously been stopped. It will simply continue gathering data.
Profiling Focus | Focus the profiling on a specific function. Note: you **cannot** put arbitrary Lua in there, just function names! E.g. `player.GetAll` will work, but `hook.GetTable().Think.Cavity` will _not_.
Bottlenecks | Shows the functions that the game has spent its most time on. The top ones are the ones that hurt your FPS most.
Top n most expensive | Perfect for finding the cause of lag spikes. Lists the functions that took a long time on specific times they were called. Differs from the Bottlenecks tab in that Bottlenecks is about *all* the times the functions were called, this tab is about the single times they ran at their slowest.
Focus button | Sets the profiling focus to the selected function
## Using FProfiler in code
FProfiler has an API, a simple one too. All functions listed below are shared:
```lua
-- Starts profiling.
-- When focus is given, the profiler will only profile the focussed upon function, and the functions it calls
FProfiler.start([focus])
-- Stops profiling
FProfiler.stop()
-- Continue profiling
FProfiler.continueProfiling()
```
All the data of the profiling sessions can be seen in the `FProfiler` menu. Because of that, there need to be **no** data retrieving functions in the API.
If you don't want to use the UI, you *probably* want the profiling in a custom format. There are some internal functions available for that. Check out `lua/fprofiler/gather.lua` and `lua/fprofiler/report`.
## About bottlenecks
When faced with performance problems, people tend to dive in the code to perform micro-optimisations. Think of localising libraries to the scope of a file, storing `LocalPlayer()` in a variable for re-use and that kind of stuff. This is a naive approach and is unlikely to get you very far.
The reason for that is very simple: micro-optimisations have **very** little effect on the eventual performance of the game. They're called micro-optimisations for a reason.
What you *should* be after is macro-optimisations, i.e. the big guys. Attacking those will give you the biggest benefit. Doubling your FPS is not uncommon when you attack the big guys.
What do I mean by macro-optimisation/the big guys you ask? Think of reducing an O(n^2) algorithm to an O(n lg n) one. Think of things like using more efficient data structures, more efficient algorithms, caching the results of complicated calculations, alternative ways to calculate things that don't give the exact right result, but give a "good enough" result and are *way faster* than the original algorithm. **THAT** kind of shit.
That's where the profiler comes in. Always mistrust what you **think** is a big performance hog is, **measure** it. Even the assumptions of people who have optimising code as their profession are often proven wrong by the profiler. Don't be smug and think you can do any better.
When working on performance, the profiler is to be your guide. The profiler is to tell you what to optimise. Do not bother with anything other than the most expensive functions for you will be wasting your time.

View File

@@ -0,0 +1,48 @@
--[[
| This file was obtained through the combined efforts
| of Madbluntz & Plymouth Antiquarian Society.
|
| Credits: lifestorm, Gregory Wayne Rossel JR.,
| Maloy, DrPepper10 @ RIP, Atle!
|
| Visit for more: https://plymouth.thetwilightzone.ru/
--]]
FProfiler = FProfiler or {}
FProfiler.Internal = {}
FProfiler.UI = {}
AddCSLuaFile()
AddCSLuaFile("fprofiler/cami.lua")
AddCSLuaFile("fprofiler/gather.lua")
AddCSLuaFile("fprofiler/report.lua")
AddCSLuaFile("fprofiler/util.lua")
AddCSLuaFile("fprofiler/prettyprint.lua")
AddCSLuaFile("fprofiler/ui/model.lua")
AddCSLuaFile("fprofiler/ui/frame.lua")
AddCSLuaFile("fprofiler/ui/clientcontrol.lua")
AddCSLuaFile("fprofiler/ui/servercontrol.lua")
include("fprofiler/cami.lua")
CAMI.RegisterPrivilege{
Name = "FProfiler",
MinAccess = "superadmin"
}
include("fprofiler/prettyprint.lua")
include("fprofiler/util.lua")
include("fprofiler/gather.lua")
include("fprofiler/report.lua")
if CLIENT then
include("fprofiler/ui/model.lua")
include("fprofiler/ui/frame.lua")
include("fprofiler/ui/clientcontrol.lua")
include("fprofiler/ui/servercontrol.lua")
else
include("fprofiler/ui/server.lua")
end

View File

@@ -0,0 +1,534 @@
--[[
| This file was obtained through the combined efforts
| of Madbluntz & Plymouth Antiquarian Society.
|
| Credits: lifestorm, Gregory Wayne Rossel JR.,
| Maloy, DrPepper10 @ RIP, Atle!
|
| Visit for more: https://plymouth.thetwilightzone.ru/
--]]
--[[
CAMI - Common Admin Mod Interface.
Makes admin mods intercompatible and provides an abstract privilege interface
for third party addons.
IMPORTANT: This is a draft script. It is very much WIP.
Follows the specification on this page:
https://docs.google.com/document/d/1QIRVcAgZfAYf1aBl_dNV_ewR6P25wze2KmUVzlbFgMI
Structures:
CAMI_USERGROUP, defines the charactaristics of a usergroup:
{
Name
string
The name of the usergroup
Inherits
string
The name of the usergroup this usergroup inherits from
}
CAMI_PRIVILEGE, defines the charactaristics of a privilege:
{
Name
string
The name of the privilege
MinAccess
string
One of the following three: user/admin/superadmin
HasAccess
function(
privilege :: CAMI_PRIVILEGE,
actor :: Player,
target :: Player
) :: bool
optional
Function that decides whether a player can execute this privilege,
optionally on another player (target).
}
]]
-- Version number in YearMonthDay format.
local version = 20150902.1
if CAMI and CAMI.Version >= version then return end
CAMI = CAMI or {}
CAMI.Version = version
--[[
usergroups
Contains the registered CAMI_USERGROUP usergroup structures.
Indexed by usergroup name.
]]
local usergroups = CAMI.GetUsergroups and CAMI.GetUsergroups() or {
user = {
Name = "user",
Inherits = "user"
},
admin = {
Name = "admin",
Inherits = "user"
},
superadmin = {
Name = "superadmin",
Inherits = "admin"
}
}
--[[
privileges
Contains the registered CAMI_PRIVILEGE privilege structures.
Indexed by privilege name.
]]
local privileges = CAMI.GetPrivileges and CAMI.GetPrivileges() or {}
--[[
CAMI.RegisterUsergroup
Registers a usergroup with CAMI.
Parameters:
usergroup
CAMI_USERGROUP
(see CAMI_USERGROUP structure)
source
any
Identifier for your own admin mod. Can be anything.
Use this to make sure CAMI.RegisterUsergroup function and the
CAMI.OnUsergroupRegistered hook don't cause an infinite loop
Return value:
CAMI_USERGROUP
The usergroup given as argument.
]]
function CAMI.RegisterUsergroup(usergroup, source)
usergroups[usergroup.Name] = usergroup
hook.Call("CAMI.OnUsergroupRegistered", nil, usergroup, source)
return usergroup
end
--[[
CAMI.UnregisterUsergroup
Unregisters a usergroup from CAMI. This will call a hook that will notify
all other admin mods of the removal.
Call only when the usergroup is to be permanently removed.
Parameters:
usergroupName
string
The name of the usergroup.
source
any
Identifier for your own admin mod. Can be anything.
Use this to make sure CAMI.UnregisterUsergroup function and the
CAMI.OnUsergroupUnregistered hook don't cause an infinite loop
Return value:
bool
Whether the unregistering succeeded.
]]
function CAMI.UnregisterUsergroup(usergroupName, source)
if not usergroups[usergroupName] then return false end
local usergroup = usergroups[usergroupName]
usergroups[usergroupName] = nil
hook.Call("CAMI.OnUsergroupUnregistered", nil, usergroup, source)
return true
end
--[[
CAMI.GetUsergroups
Retrieves all registered usergroups.
Return value:
Table of CAMI_USERGROUP, indexed by their names.
]]
function CAMI.GetUsergroups()
return usergroups
end
--[[
CAMI.GetUsergroup
Receives information about a usergroup.
Return value:
CAMI_USERGROUP
Returns nil when the usergroup does not exist.
]]
function CAMI.GetUsergroup(usergroupName)
return usergroups[usergroupName]
end
--[[
CAMI.UsergroupInherits
Returns true when usergroupName1 inherits usergroupName2.
Note that usergroupName1 does not need to be a direct child.
Every usergroup trivially inherits itself.
Parameters:
usergroupName1
string
The name of the usergroup that is queried.
usergroupName2
string
The name of the usergroup of which is queried whether usergroupName1
inherits from.
Return value:
bool
Whether usergroupName1 inherits usergroupName2.
]]
function CAMI.UsergroupInherits(usergroupName1, usergroupName2)
repeat
if usergroupName1 == usergroupName2 then return true end
usergroupName1 = usergroups[usergroupName1] and
usergroups[usergroupName1].Inherits or
usergroupName1
until not usergroups[usergroupName1] or
usergroups[usergroupName1].Inherits == usergroupName1
-- One can only be sure the usergroup inherits from user if the
-- usergroup isn't registered.
return usergroupName1 == usergroupName2 or usergroupName2 == "user"
end
--[[
CAMI.InheritanceRoot
All usergroups must eventually inherit either user, admin or superadmin.
Regardless of what inheritance mechism an admin may or may not have, this
always applies.
This method always returns either user, admin or superadmin, based on what
usergroups eventually inherit.
Parameters:
usergroupName
string
The name of the usergroup of which the root of inheritance is
requested
Return value:
string
The name of the root usergroup (either user, admin or superadmin)
]]
function CAMI.InheritanceRoot(usergroupName)
if not usergroups[usergroupName] then return end
local inherits = usergroups[usergroupName].Inherits
while inherits ~= usergroups[usergroupName].Inherits do
usergroupName = usergroups[usergroupName].Inherits
end
return usergroupName
end
--[[
CAMI.RegisterPrivilege
Registers a privilege with CAMI.
Note: do NOT register all your admin mod's privileges with this function!
This function is for third party addons to register privileges
with admin mods, not for admin mods sharing the privileges amongst one
another.
Parameters:
privilege
CAMI_PRIVILEGE
See CAMI_PRIVILEGE structure.
Return value:
CAMI_PRIVILEGE
The privilege given as argument.
]]
function CAMI.RegisterPrivilege(privilege)
privileges[privilege.Name] = privilege
hook.Call("CAMI.OnPrivilegeRegistered", nil, privilege)
return privilege
end
--[[
CAMI.UnregisterPrivilege
Unregisters a privilege from CAMI. This will call a hook that will notify
all other admin mods of the removal.
Call only when the privilege is to be permanently removed.
Parameters:
privilegeName
string
The name of the privilege.
Return value:
bool
Whether the unregistering succeeded.
]]
function CAMI.UnregisterPrivilege(privilegeName)
if not privileges[privilegeName] then return false end
local privilege = privileges[privilegeName]
privileges[privilegeName] = nil
hook.Call("CAMI.OnPrivilegeUnregistered", nil, privilege)
return true
end
--[[
CAMI.GetPrivileges
Retrieves all registered privileges.
Return value:
Table of CAMI_PRIVILEGE, indexed by their names.
]]
function CAMI.GetPrivileges()
return privileges
end
--[[
CAMI.GetPrivilege
Receives information about a privilege.
Return value:
CAMI_PRIVILEGE when the privilege exists.
nil when the privilege does not exist.
]]
function CAMI.GetPrivilege(privilegeName)
return privileges[privilegeName]
end
--[[
CAMI.PlayerHasAccess
Queries whether a certain player has the right to perform a certain action.
Note: this function does NOT return an immediate result!
The result is in the callback!
Parameters:
actorPly
Player
The player of which is requested whether they have the privilege.
privilegeName
string
The name of the privilege.
callback
function(bool, string)
This function will be called with the answer. The bool signifies the
yes or no answer as to whether the player is allowed. The string
will optionally give a reason.
targetPly
Optional.
The player on which the privilege is executed.
extraInfoTbl
Optional.
Table containing extra information.
Officially supported members:
Fallback
string
Either of user/admin/superadmin. When no admin mod replies,
the decision is based on the admin status of the user.
Defaults to admin if not given.
IgnoreImmunity
bool
Ignore any immunity mechanisms an admin mod might have.
CommandArguments
table
Extra arguments that were given to the privilege command.
Return value:
None, the answer is given in the callback function in order to allow
for the admin mod to perform e.g. a database lookup.
]]
-- Default access handler
local defaultAccessHandler = {["CAMI.PlayerHasAccess"] =
function(_, actorPly, privilegeName, callback, _, extraInfoTbl)
-- The server always has access in the fallback
if not IsValid(actorPly) then return callback(true, "Fallback.") end
local priv = privileges[privilegeName]
local fallback = extraInfoTbl and (
not extraInfoTbl.Fallback and actorPly:IsAdmin() or
extraInfoTbl.Fallback == "user" and true or
extraInfoTbl.Fallback == "admin" and actorPly:IsAdmin() or
extraInfoTbl.Fallback == "superadmin" and actorPly:IsSuperAdmin())
if not priv then return callback(fallback, "Fallback.") end
callback(
priv.MinAccess == "user" or
priv.MinAccess == "admin" and actorPly:IsAdmin() or
priv.MinAccess == "superadmin" and actorPly:IsSuperAdmin()
, "Fallback.")
end,
["CAMI.SteamIDHasAccess"] =
function(_, _, _, callback)
callback(false, "No information available.")
end
}
function CAMI.PlayerHasAccess(actorPly, privilegeName, callback, targetPly,
extraInfoTbl)
hook.Call("CAMI.PlayerHasAccess", defaultAccessHandler, actorPly,
privilegeName, callback, targetPly, extraInfoTbl)
end
--[[
CAMI.GetPlayersWithAccess
Finds the list of currently joined players who have the right to perform a
certain action.
NOTE: this function will NOT return an immediate result!
The result is in the callback!
Parameters:
privilegeName
string
The name of the privilege.
callback
function(players)
This function will be called with the list of players with access.
targetPly
Optional.
The player on which the privilege is executed.
extraInfoTbl
Optional.
Table containing extra information.
Officially supported members:
Fallback
string
Either of user/admin/superadmin. When no admin mod replies,
the decision is based on the admin status of the user.
Defaults to admin if not given.
IgnoreImmunity
bool
Ignore any immunity mechanisms an admin mod might have.
CommandArguments
table
Extra arguments that were given to the privilege command.
]]
function CAMI.GetPlayersWithAccess(privilegeName, callback, targetPly,
extraInfoTbl)
local allowedPlys = {}
local allPlys = player.GetAll()
local countdown = #allPlys
local function onResult(ply, hasAccess, _)
countdown = countdown - 1
if hasAccess then table.insert(allowedPlys, ply) end
if countdown == 0 then callback(allowedPlys) end
end
for _, ply in pairs(allPlys) do
CAMI.PlayerHasAccess(ply, privilegeName,
function(...) onResult(ply, ...) end,
targetPly, extraInfoTbl)
end
end
--[[
CAMI.SteamIDHasAccess
Queries whether a player with a steam ID has the right to perform a certain
action.
Note: the player does not need to be in the server for this to
work.
Note: this function does NOT return an immediate result!
The result is in the callback!
Parameters:
actorSteam
Player
The SteamID of the player of which is requested whether they have
the privilege.
privilegeName
string
The name of the privilege.
callback
function(bool, string)
This function will be called with the answer. The bool signifies the
yes or no answer as to whether the player is allowed. The string
will optionally give a reason.
targetSteam
Optional.
The SteamID of the player on which the privilege is executed.
extraInfoTbl
Optional.
Table containing extra information.
Officially supported members:
IgnoreImmunity
bool
Ignore any immunity mechanisms an admin mod might have.
CommandArguments
table
Extra arguments that were given to the privilege command.
Return value:
None, the answer is given in the callback function in order to allow
for the admin mod to perform e.g. a database lookup.
]]
function CAMI.SteamIDHasAccess(actorSteam, privilegeName, callback,
targetSteam, extraInfoTbl)
hook.Call("CAMI.SteamIDHasAccess", defaultAccessHandler, actorSteam,
privilegeName, callback, targetSteam, extraInfoTbl)
end
--[[
CAMI.SignalUserGroupChanged
Signify that your admin mod has changed the usergroup of a player. This
function communicates to other admin mods what it thinks the usergroup
of a player should be.
Listen to the hook to receive the usergroup changes of other admin mods.
Parameters:
ply
Player
The player for which the usergroup is changed
old
string
The previous usergroup of the player.
new
string
The new usergroup of the player.
source
any
Identifier for your own admin mod. Can be anything.
]]
function CAMI.SignalUserGroupChanged(ply, old, new, source)
hook.Call("CAMI.PlayerUsergroupChanged", nil, ply, old, new, source)
end
--[[
CAMI.SignalSteamIDUserGroupChanged
Signify that your admin mod has changed the usergroup of a disconnected
player. This communicates to other admin mods what it thinks the usergroup
of a player should be.
Listen to the hook to receive the usergroup changes of other admin mods.
Parameters:
ply
string
The steam ID of the player for which the usergroup is changed
old
string
The previous usergroup of the player.
new
string
The new usergroup of the player.
source
any
Identifier for your own admin mod. Can be anything.
]]
function CAMI.SignalSteamIDUserGroupChanged(steamId, old, new, source)
hook.Call("CAMI.SteamIDUsergroupChanged", nil, steamId, old, new, source)
end

View File

@@ -0,0 +1,267 @@
--[[
| This file was obtained through the combined efforts
| of Madbluntz & Plymouth Antiquarian Society.
|
| Credits: lifestorm, Gregory Wayne Rossel JR.,
| Maloy, DrPepper10 @ RIP, Atle!
|
| Visit for more: https://plymouth.thetwilightzone.ru/
--]]
local timeMeasurementFunc = SysTime
-- Helper function
-- Get all local variables
local NIL = {}
setmetatable(NIL, {__tostring = function() return "nil" end})
--[[-------------------------------------------------------------------------
Call counts:
registers how often function have been called
---------------------------------------------------------------------------]]
local callcounts = {}
-- Gets the call counts
FProfiler.Internal.getCallCounts = function() return callcounts end
-- Resets the call counts
function FProfiler.Internal.resetCallCounts()
callcounts = {}
end
--[[-------------------------------------------------------------------------
Inclusive function times
Keeps track of how long functions take in total
i.e. average time between the start and return of a function * times called
This includes the time that any function called by this function takes
(that's what the "inclusive" refers to).
Note: recursive calls are not counted double
---------------------------------------------------------------------------]]
local inclusiveTimes = {}
-- Gets the inclusive times
FProfiler.Internal.getInclusiveTimes = function() return inclusiveTimes end
-- Resets the inclusive times
function FProfiler.Internal.resetInclusiveTimes()
inclusiveTimes = {}
end
--[[-------------------------------------------------------------------------
Top n most expensive single function calls
Keeps track of the functions that took the longest time to run
Note: functions can appear in this list at most once
---------------------------------------------------------------------------]]
local mostExpensiveSingleCalls = {}
-- Gets most expensive single calls
FProfiler.Internal.getMostExpensiveSingleCalls = function() return mostExpensiveSingleCalls end
-- Dictionary to make sure the same function doesn't appear multiple times
-- in the top n
local mostExpensiveSingleDict = {}
function FProfiler.Internal.resetMostExpensiveSingleCalls()
for i = 1, 50 do mostExpensiveSingleCalls[i] = {runtime = 0} end
mostExpensiveSingleDict = {}
end
-- Initial empty list
FProfiler.Internal.resetMostExpensiveSingleCalls()
--[[-------------------------------------------------------------------------
Function information
Using debug.getinfo on a function object won't give you any function names
that's because functions can have multiple names.
Luckily, when the functions are called, debug.getinfo(level) gives the
function name and scope
---------------------------------------------------------------------------]]
local functionNames = {}
FProfiler.Internal.getFunctionNames = function() return functionNames end
--[[-------------------------------------------------------------------------
Recursion depth
Used internally to make sure recursive functions' times aren't counted
multiple times
---------------------------------------------------------------------------]]
local recursiveCount = {}
--[[-------------------------------------------------------------------------
Function start times
Used internally to keep track of when functions were called
---------------------------------------------------------------------------]]
local startTimes = {}
--[[-------------------------------------------------------------------------
Lua code event handlers
---------------------------------------------------------------------------]]
-- The recursion depth of the function that is in focus.
-- Only applies when profiling a specific function (i.e. laying focus upon)
local focusDepth = 0
-- Called when a function in the code is called
local function registerFunctionCall(funcInfo)
local func = funcInfo.func
-- Update call counts
callcounts[func] = (callcounts[func] or 0) + 1
-- Increase recursion depth for this function
recursiveCount[func] = (recursiveCount[func] or 0) + 1
-- Store function info
local funcname = funcInfo.name or ""
functionNames[func] = functionNames[func] or {}
functionNames[func][funcname] = functionNames[func][funcname] or
{ namewhat = funcInfo.namewhat,
nparams = funcInfo.nparams
}
local time = timeMeasurementFunc()
-- Update inclusive function times,
-- only when we're on the first recursive call
if recursiveCount[func] == 1 then
startTimes[func] = time
end
end
-- Called when a function returns
local function registerReturn(funcInfo)
local time = timeMeasurementFunc()
local func = funcInfo.func
local runtime = time - startTimes[func]
-- Update inclusive function time
-- Only update on the topmost call, to prevent recursive
-- calls for being counted multiple times.
if recursiveCount[func] == 1 then
inclusiveTimes[func] = (inclusiveTimes[func] or 0) + runtime
end
-- Maintain recursion depth
recursiveCount[func] = recursiveCount[func] - 1
-- Update top n list
-- This path will be taken most often: the function isn't special
-- Also only counts the top recursion
if runtime <= mostExpensiveSingleCalls[50].runtime or recursiveCount[func] > 1 then return end
-- If the function already appears in the top 10, replace it or discard the result
if mostExpensiveSingleDict[func] then
local i = mostExpensiveSingleDict[func]
-- Discard this info
if runtime < mostExpensiveSingleCalls[i].runtime then return end
-- Update the entry
mostExpensiveSingleCalls[i].runtime = runtime
mostExpensiveSingleCalls[i].info = funcInfo
mostExpensiveSingleCalls[i].func = func
-- Move the updated entry up the top 10 list if applicable
while i > 1 and runtime > mostExpensiveSingleCalls[i - 1].runtime do
mostExpensiveSingleDict[mostExpensiveSingleCalls[i - 1].func] = i
mostExpensiveSingleCalls[i - 1], mostExpensiveSingleCalls[i] = mostExpensiveSingleCalls[i], mostExpensiveSingleCalls[i - 1]
i = i - 1
end
mostExpensiveSingleDict[func] = i
return
end
-- Knowing that the function belongs in the top n, find its position
local i = 50
while i >= 1 and runtime > mostExpensiveSingleCalls[i].runtime do
-- Update the dictionary
-- All functions faster than the current one move down the list
if not mostExpensiveSingleCalls[i].func then i = i - 1 continue end
mostExpensiveSingleDict[mostExpensiveSingleCalls[i].func] = i + 1
i = i - 1
end
-- Insert the expensive call in the top n
mostExpensiveSingleDict[func] = i + 1
table.insert(mostExpensiveSingleCalls, i + 1,
{
func = func,
runtime = runtime,
info = funcInfo,
})
-- What was previously the 50th most expensive function
-- is now kicked out of the top 10
if mostExpensiveSingleCalls[51].func then
mostExpensiveSingleDict[mostExpensiveSingleCalls[51].func] = nil
end
mostExpensiveSingleCalls[51] = nil
end
-- Called on any Lua event
local function onLuaEvent(event, focus)
local info = debug.getinfo(3)
local func = info.func
if event == "call" or event == "tail call" then
-- Only track the focussed function and the functions
-- called by the focussed function
if focus == func then focusDepth = focusDepth + 1 end
if focus and focusDepth == 0 then return end
registerFunctionCall(info)
else
-- Functions that return right after the call to FProfiler.Internal.start()
-- are not to be counted
if not recursiveCount[func] or recursiveCount[func] == 0 then return end
if focus == func then focusDepth = focusDepth - 1 end
if focus and focusDepth == 0 then return end
registerReturn(info)
end
end
--[[-------------------------------------------------------------------------
Profiling control
---------------------------------------------------------------------------]]
-- Start profiling
-- focus: only measure data of everything that happens within a certain function
function FProfiler.Internal.start(focus)
-- Empty start times, so unfinished functions aren't
-- registered as returns on a second profiling session
-- local time = SysTime()
-- for k,v in pairs(startTimes) do startTimes[k] = time end
table.Empty(startTimes)
table.Empty(recursiveCount)
debug.sethook(function(event) onLuaEvent(event, focus) end, "cr")
end
-- Stop profiling
function FProfiler.Internal.stop()
debug.sethook()
end
-- Reset all profiling data
function FProfiler.Internal.reset()
FProfiler.Internal.resetCallCounts()
FProfiler.Internal.resetInclusiveTimes()
FProfiler.Internal.resetMostExpensiveSingleCalls()
end

View File

@@ -0,0 +1,594 @@
--[[
| This file was obtained through the combined efforts
| of Madbluntz & Plymouth Antiquarian Society.
|
| Credits: lifestorm, Gregory Wayne Rossel JR.,
| Maloy, DrPepper10 @ RIP, Atle!
|
| Visit for more: https://plymouth.thetwilightzone.ru/
--]]
-- Based on MDave's thing
-- https://gist.github.com/mentlerd/d56ad9e6361f4b86af84
if SERVER then AddCSLuaFile() end
local type_weight = {
[TYPE_FUNCTION] = 1,
[TYPE_TABLE] = 2,
}
local type_colors = {
[TYPE_BOOL] = Color(175, 130, 255),
[TYPE_NUMBER] = Color(175, 130, 255),
[TYPE_STRING] = Color(230, 220, 115),
[TYPE_FUNCTION] = Color(100, 220, 240)
}
local color_neutral = Color(220, 220, 220)
local color_name = Color(260, 150, 30)
local color_reference = Color(150, 230, 50)
local color_comment = Color( 30, 210, 30)
-- 'nil' value
local NIL = {}
-- Localise for faster access
local pcall = pcall
local string_len = string.len
local string_sub = string.sub
local string_find = string.find
local table_concat = table.concat
local table_insert = table.insert
local table_sort = table.sort
-- Stream interface
local gMsgF -- Print fragment
local gMsgN -- Print newline
local gMsgC -- Set print color
local PrintLocals, gBegin, gFinish, PrintTableGrep
do
local grep_color = Color(235, 70, 70)
-- Grep parameters (static between gBegin/gEnd)
local grep
local grep_raw
local grep_proximity
-- Current line parameters
local buffer
local colors
local markers
local baseColor
local currColor
local length
-- History
local history
local remain
-- Actual printing
local function gCheckMatch( buffer )
local raw = table_concat(buffer)
return raw, string_find(raw, grep, 0, grep_raw)
end
local function gFlushEx( raw, markers, colors, baseColor )
-- Print entire buffer
local len = string_len(raw)
-- Keep track of the current line properties
local index = 1
local marker = 1
local currColor = baseColor
-- Method to print to a preset area
local function printToIndex( limit, color )
local mark = markers and markers[marker]
-- Print all marker areas until we would overshoot
while mark and mark < limit do
-- Catch up to the marker
MsgC(color or currColor or color_neutral, string_sub(raw, index, mark))
index = mark +1
-- Set new color
currColor = colors[marker]
-- Select next marker
marker = marker +1
mark = markers[marker]
end
-- Print the remaining between the last marker and the limit
MsgC(color or currColor or color_neutral, string_sub(raw, index, limit))
index = limit +1
end
-- Grep!
local match, last = 1
local from, to = string_find(raw, grep, 0, grep_raw)
while from do
printToIndex(from -1)
printToIndex(to, grep_color)
last = to +1
from, to = string_find(raw, grep, last, grep_raw)
end
printToIndex(len)
MsgN()
end
local function gCommit()
if grep_proximity then
-- Check if the line has at least one match
local raw, match = gCheckMatch(buffer)
if match then
-- Divide matches
if history[grep_proximity] then
MsgN("...")
end
-- Flush history
if grep_proximity ~= 0 then
local len = #history
for index = len -1, 1, -1 do
local entry = history[index]
history[index] = nil
gFlushEx( entry[1], entry[2], entry[3], entry[4] )
end
history[len] = nil
end
-- Flush line, allow next X lines to get printed
gFlushEx( raw, markers, colors, baseColor )
remain = grep_proximity -1
history[grep_proximity +1] = nil
elseif remain > 0 then
-- Flush immediately
gFlushEx( raw, markers, colors, baseColor )
remain = remain -1
else
-- Store in history
table_insert(history, 1, {raw, markers, colors, baseColor})
history[grep_proximity +1] = nil
end
else
-- Flush anyway
gFlushEx( table_concat(buffer), markers, colors, baseColor )
end
-- Reset state
length = 0
buffer = {}
markers = nil
colors = nil
baseColor = nil
currColor = nil
end
-- State machine
function gBegin( new, prox )
grep = isstring(new) and new
if grep then
grep_raw = not pcall(string_find, ' ', grep)
grep_proximity = isnumber(prox) and prox
-- Reset everything
buffer = {}
history = {}
end
length = 0
remain = 0
baseColor = nil
currColor = nil
end
function gFinish()
if grep_proximity and history and history[1] then
MsgN("...")
end
-- Free memory
buffer = nil
markers = nil
colors = nil
history = nil
end
function gMsgC( color )
if grep then
-- Try to save some memory by not immediately allocating colors
if length == 0 then
baseColor = color
return
end
-- Record color change
if color ~= currColor then
if not markers then
markers = {}
colors = {}
end
-- Record color change
table_insert(markers, length)
table_insert(colors, color)
end
end
currColor = color
end
function gMsgF( str )
if grep then
-- Split multiline fragments to separate ones
local fragColor = currColor or baseColor
local last = 1
local from, to = string_find(str, '\n')
while from do
local frag = string_sub(str, last, from -1)
local len = from - last
-- Merge fragment to the line
length = length + len
table_insert(buffer, frag)
-- Print finished line
gCommit()
-- Assign base color as previous fragColor
baseColor = fragColor
-- Look for more
last = to +1
from, to = string_find(str, '\n', last)
end
-- Push last fragment
local frag = string_sub(str, last)
local len = string_len(str) - last +1
length = length + len
table_insert(buffer, frag)
else
-- Push immediately
MsgC(currColor or baseColor or color_neutral, str)
end
end
function gMsgN()
-- Print everything in the buffer
if grep then
gCommit()
else
MsgN()
end
baseColor = nil
currColor = nil
end
end
local function InternalPrintValue( value )
-- 'nil' values can also be printed
if value == NIL then
gMsgC(color_comment)
gMsgF("nil")
return
end
local color = type_colors[ TypeID(value) ]
-- For strings, place quotes
if isstring(value) then
if string_len(value) <= 1 then
value = string.format([['%s']], value)
else
value = string.format([["%s"]], value)
end
gMsgC(color)
gMsgF(value)
return
end
-- Workaround for userdata not using MetaName
if string_sub(tostring(value), 0, 8) == "userdata" then
local meta = getmetatable(value)
if meta and meta.MetaName then
value = string.format("%s: %p", meta.MetaName, value)
end
end
-- General print
gMsgC(color)
gMsgF(tostring(value))
-- For functions append source info
if isfunction(value) then
local info = debug.getinfo(value, 'S')
local aux
if info.what == 'C' then
aux = "\t-- [C]: -1"
else
if info.linedefined ~= info.lastlinedefined then
aux = string.format("\t-- [%s]: %i-%i", info.short_src, info.linedefined, info.lastlinedefined)
else
aux = string.format("\t-- [%s]: %i", info.short_src, info.linedefined)
end
end
gMsgC(color_comment)
gMsgF(aux)
end
end
-- Associated to object keys
local objID
local function isprimitive( value )
local id = TypeID(value)
return id <= TYPE_FUNCTION and id ~= TYPE_TABLE
end
local function InternalPrintTable( table, path, prefix, names, todo )
-- Collect keys and some info about them
local keyList = {}
local keyStr = {}
local keyCount = 0
for key, value in pairs( table ) do
-- Add to key list for later sorting
table_insert(keyList, key)
-- Describe key as string
if isprimitive(key) then
keyStr[key] = tostring(key)
else
-- Lookup already known name
local name = names[key]
-- Assign a new unique identifier
if not name then
objID = objID +1
name = string.format("%s: obj #%i", tostring(key), objID)
names[key] = name
todo[key] = true
end
-- Substitute object with name
keyStr[key] = name
end
keyCount = keyCount +1
end
-- Exit early for empty tables
if keyCount == 0 then
return
end
-- Determine max key length
local keyLen = 4
for key, str in pairs(keyStr) do
keyLen = math.max(keyLen, string.len(str))
end
-- Sort table keys
if keyCount > 1 then
table_sort( keyList, function( A, B )
-- Sort numbers numerically correct
if isnumber(A) and isnumber(B) then
return A < B
end
-- Weight types
local wA = type_weight[ TypeID( table[A] ) ] or 0
local wB = type_weight[ TypeID( table[B] ) ] or 0
if wA ~= wB then
return wA < wB
end
-- Order by string representation
return keyStr[A] < keyStr[B]
end )
end
-- Determine the next level ident
local new_prefix = string.format( "%s║%s", prefix, string.rep(' ', keyLen) )
-- Mark object as done
todo[table] = nil
-- Start describing table
for index, key in ipairs(keyList) do
local value = table[key]
-- Assign names to already described keys/values
local kName = names[key]
local vName = names[value]
-- Decide to either fully describe, or print the value
local describe = not isprimitive(value) and ( not vName or todo[value] )
-- Ident
gMsgF(prefix)
-- Fancy table guides
local moreLines = (index ~= keyCount) or describe
if index == 1 then
gMsgF(moreLines and '' or '')
else
gMsgF(moreLines and '' or '')
end
-- Print key
local sKey = kName or keyStr[key]
gMsgC(kName and color_reference or color_name)
gMsgF(sKey)
-- Describe non primitives
describe = istable(value) and ( not names[value] or todo[value] ) and value ~= NIL
-- Print key postfix
local padding = keyLen - string.len(sKey)
local postfix = string.format(describe and ":%s" or "%s = ", string.rep(' ', padding))
gMsgC(color_neutral)
gMsgF(postfix)
-- Print the value
if describe then
gMsgN()
-- Expand access path
local new_path = sKey
if isnumber(key) or kName then
new_path = string.format("%s[%s]", path or '', key)
elseif path then
new_path = string.format("%s.%s", path, new_path)
end
-- Name the object to mark it done
names[value] = names[value] or new_path
-- Describe
InternalPrintTable(value, new_path, new_prefix, names, todo)
else
-- Print the value (or the reference name)
if vName and not todo[value] then
gMsgC(color_reference)
gMsgF(string.format("ref: %s",vName))
else
InternalPrintValue(value)
end
gMsgN()
end
end
end
function PrintTableGrep( table, grep, proximity )
local base = {
[_G] = "_G",
[table] = "root"
}
gBegin(grep, proximity)
objID = 0
InternalPrintTable(table, nil, "", base, {})
gFinish()
end
function PrintLocals( level )
local level = level or 2
local hash = {}
for index = 1, 255 do
local name, value = debug.getlocal(2, index)
if not name then
break
end
if value == nil then
value = NIL
end
hash[name] = value
end
PrintTableGrep( hash )
end
function show(...)
local n = select('#', ...)
local tbl = {...}
for i = 1, n do
if istable(tbl[i]) then MsgN(tostring(tbl[i])) PrintTableGrep(tbl[i])
else InternalPrintValue(tbl[i]) MsgN() end
end
end
-- Hacky way of creating a pretty string from the above code
-- because I don't feel like refactoring the entire thing
local strResult
local toStringMsgF = function(txt)
table.insert(strResult, txt)
end
local toStringMsgN = function()
table.insert(strResult, "\n")
end
local toStringMsgC = function(_, txt)
table.insert(strResult, txt)
end
function showStr(...)
local oldF, oldN, oldMsgC, oldMsgN = gMsgF, gMsgN, MsgC, MsgN
gMsgF, gMsgN, MsgC, MsgN = toStringMsgF, toStringMsgN, toStringMsgC, toStringMsgN
strResult = {}
show(...)
gMsgF, gMsgN, MsgC, MsgN = oldF, oldN, oldMsgC, oldMsgN
return table.concat(strResult, "")
end

View File

@@ -0,0 +1,114 @@
--[[
| This file was obtained through the combined efforts
| of Madbluntz & Plymouth Antiquarian Society.
|
| Credits: lifestorm, Gregory Wayne Rossel JR.,
| Maloy, DrPepper10 @ RIP, Atle!
|
| Visit for more: https://plymouth.thetwilightzone.ru/
--]]
local function getData()
local callCounts = FProfiler.Internal.getCallCounts()
local inclusiveTimes = FProfiler.Internal.getInclusiveTimes()
local funcNames = FProfiler.Internal.getFunctionNames()
local data = {}
for func, called in pairs(callCounts) do
local row = {}
row.func = func
row.info = debug.getinfo(func, "nfS")
row.total_called = called
row.total_time = inclusiveTimes[func] or 0
row.average_time = row.total_time / row.total_called
row.name, row.namewhat = nil, nil
row.names = {}
for name, namedata in pairs(funcNames[func]) do
table.insert(row.names, {name = name, namewhat = namedata.namewhat, nparams = namedata.nparams})
end
table.insert(data, row)
end
return data
end
local function cull(data, count)
if not count then return data end
for i = count + 1, #data do
data[i] = nil
end
return data
end
--[[-------------------------------------------------------------------------
The functions that are called most often
Their implementations are O(n lg n),
which is probably suboptimal but not worth my time optimising.
---------------------------------------------------------------------------]]
function FProfiler.Internal.mostOftenCalled(count)
local sorted = getData()
table.SortByMember(sorted, "total_called")
return cull(sorted, count)
end
--[[-------------------------------------------------------------------------
The functions that take the longest time in total
---------------------------------------------------------------------------]]
function FProfiler.Internal.mostTimeInclusive(count)
local sorted = getData()
table.SortByMember(sorted, "total_time")
return cull(sorted, count)
end
--[[-------------------------------------------------------------------------
The functions that take the longest average time
---------------------------------------------------------------------------]]
function FProfiler.Internal.mostTimeInclusiveAverage(count)
local sorted = getData()
table.SortByMember(sorted, "average_time")
return cull(sorted, count)
end
--[[-------------------------------------------------------------------------
Get the top <count> of most often called, time inclusive and average
NOTE: This will almost definitely return more than <count> results.
Up to three times <count> is possible.
---------------------------------------------------------------------------]]
function FProfiler.Internal.getAggregatedResults(count)
count = count or 100
local dict = {}
local mostTime = FProfiler.Internal.mostTimeInclusive(count)
for i = 1, #mostTime do dict[mostTime[i].func] = true end
local mostAvg = FProfiler.Internal.mostTimeInclusiveAverage(count)
for i = 1, #mostAvg do
if dict[mostAvg[i].func] then continue end
dict[mostAvg[i].func] = true
table.insert(mostTime, mostAvg[i])
end
local mostCalled = FProfiler.Internal.mostOftenCalled(count)
for i = 1, #mostCalled do
if dict[mostCalled[i].func] then continue end
dict[mostCalled[i].func] = true
table.insert(mostTime, mostCalled[i])
end
table.SortByMember(mostTime, "total_time")
return mostTime
end

View File

@@ -0,0 +1,110 @@
--[[
| This file was obtained through the combined efforts
| of Madbluntz & Plymouth Antiquarian Society.
|
| Credits: lifestorm, Gregory Wayne Rossel JR.,
| Maloy, DrPepper10 @ RIP, Atle!
|
| Visit for more: https://plymouth.thetwilightzone.ru/
--]]
local get, update, onUpdate = FProfiler.UI.getModelValue, FProfiler.UI.updateModel, FProfiler.UI.onModelUpdate
--[[-------------------------------------------------------------------------
(Re)start clientside profiling
---------------------------------------------------------------------------]]
local function restartProfiling()
if get({"client", "shouldReset"}) then
FProfiler.Internal.reset()
update({"client", "recordTime"}, 0)
end
local focus = get({"client", "focusObj"})
update({"client", "sessionStart"}, CurTime())
update({"client", "sessionStartSysTime"}, SysTime())
FProfiler.Internal.start(focus)
end
--[[-------------------------------------------------------------------------
Stop profiling
---------------------------------------------------------------------------]]
local function stopProfiling()
FProfiler.Internal.stop()
local newTime = get({"client", "recordTime"}) + SysTime() - (get({"client", "sessionStartSysTime"}) or 0)
-- Get the aggregated data
local mostTime = FProfiler.Internal.getAggregatedResults(100)
update({"client", "bottlenecks"}, mostTime)
update({"client", "topLagSpikes"}, FProfiler.Internal.getMostExpensiveSingleCalls())
update({"client", "recordTime"}, newTime)
update({"client", "sessionStart"}, nil)
update({"client", "sessionStartSysTime"}, nil)
end
--[[-------------------------------------------------------------------------
Start/stop recording when the recording status is changed
---------------------------------------------------------------------------]]
onUpdate({"client", "status"}, function(new, old)
if new == old then return end
(new == "Started" and restartProfiling or stopProfiling)()
end)
--[[-------------------------------------------------------------------------
Update the current selected focus object when data is entered
---------------------------------------------------------------------------]]
onUpdate({"client", "focusStr"}, function(new)
update({"client", "focusObj"}, FProfiler.funcNameToObj(new))
end)
--[[-------------------------------------------------------------------------
Update info when a different line is selected
---------------------------------------------------------------------------]]
onUpdate({"client", "currentSelected"}, function(new)
if not new or not new.info or not new.info.linedefined or not new.info.lastlinedefined or not new.info.short_src then return end
update({"client", "sourceText"}, FProfiler.readSource(new.info.short_src, new.info.linedefined, new.info.lastlinedefined))
end)
--[[-------------------------------------------------------------------------
When a function is to be printed to console
---------------------------------------------------------------------------]]
onUpdate({"client", "toConsole"}, function(data)
if not data then return end
update({"client", "toConsole"}, nil)
show(data)
file.CreateDir("fprofiler")
file.Write("fprofiler/profiledata.txt", showStr(data))
MsgC(Color(200, 200, 200), "-----", Color(120, 120, 255), "NOTE", Color(200, 200, 200), "---------------\n")
MsgC(Color(200, 200, 200), "If the above function does not fit in console, you can find it in data/fprofiler/profiledata.txt\n\n")
end)
--[[-------------------------------------------------------------------------
API function: start profiling
---------------------------------------------------------------------------]]
function FProfiler.start(focus)
update({"client", "focusStr"}, tostring(focus))
update({"client", "focusObj"}, focus)
update({"client", "shouldReset"}, true)
update({"client", "status"}, "Started")
end
--[[-------------------------------------------------------------------------
API function: stop profiling
---------------------------------------------------------------------------]]
function FProfiler.stop()
update({"client", "status"}, "Stopped")
end
--[[-------------------------------------------------------------------------
API function: continue profiling
---------------------------------------------------------------------------]]
function FProfiler.continueProfiling()
update({"client", "shouldReset"}, false)
update({"client", "status"}, "Started")
end

View File

@@ -0,0 +1,475 @@
--[[
| This file was obtained through the combined efforts
| of Madbluntz & Plymouth Antiquarian Society.
|
| Credits: lifestorm, Gregory Wayne Rossel JR.,
| Maloy, DrPepper10 @ RIP, Atle!
|
| Visit for more: https://plymouth.thetwilightzone.ru/
--]]
--[[-------------------------------------------------------------------------
The panel that contains the realm switcher
---------------------------------------------------------------------------]]
local REALMPANEL = {}
function REALMPANEL:Init()
self:DockPadding(0, 0, 0, 0)
self:DockMargin(0, 0, 5, 0)
self.realmLabel = vgui.Create("DLabel", self)
self.realmLabel:SetDark(true)
self.realmLabel:SetText("Realm:")
self.realmLabel:SizeToContents()
self.realmLabel:Dock(TOP)
self.realmbox = vgui.Create("DComboBox", self)
self.realmbox:AddChoice("Client")
self.realmbox:AddChoice("Server")
self.realmbox:Dock(TOP)
FProfiler.UI.onModelUpdate("realm", function(new)
self.realmbox.selected = new == "client" and 1 or 2
self.realmbox:SetText(new == "client" and "Client" or "Server")
end)
FProfiler.UI.onModelUpdate("serverAccess", function(hasAccess)
self.realmbox:SetDisabled(not hasAccess)
if not hasAccess and self.realmbox.selected == 2 then
FProfiler.UI.updateModel("realm", "client")
end
end)
self.realmbox.OnSelect = function(_, _, value) FProfiler.UI.updateModel("realm", string.lower(value)) end
end
function REALMPANEL:PerformLayout()
self.realmLabel:SizeToContents()
local top = ( self:GetTall() - self.realmLabel:GetTall() - self.realmbox:GetTall()) * 0.5
self:DockPadding(0, top, 0, 0)
end
derma.DefineControl("FProfileRealmPanel", "", REALMPANEL, "Panel")
--[[-------------------------------------------------------------------------
The little red or green indicator that indicates whether the focussing
function is correct
---------------------------------------------------------------------------]]
local FUNCINDICATOR = {}
function FUNCINDICATOR:Init()
self:SetTall(5)
self.color = Color(0, 0, 0, 0)
end
function FUNCINDICATOR:Paint()
draw.RoundedBox(0, 0, 0, self:GetWide(), self:GetTall(), self.color)
end
derma.DefineControl("FProfileFuncIndicator", "", FUNCINDICATOR, "DPanel")
--[[-------------------------------------------------------------------------
The panel that contains the focus text entry and the focus indicator
---------------------------------------------------------------------------]]
local FOCUSPANEL = {}
function FOCUSPANEL:Init()
self:DockPadding(0, 0, 0, 0)
self:DockMargin(0, 0, 5, 0)
self.focusLabel = vgui.Create("DLabel", self)
self.focusLabel:SetDark(true)
self.focusLabel:SetText("Profiling Focus:")
self.focusLabel:SizeToContents()
self.focusLabel:Dock(TOP)
self.funcIndicator = vgui.Create("FProfileFuncIndicator", self)
self.funcIndicator:Dock(BOTTOM)
self.focusBox = vgui.Create("DTextEntry", self)
self.focusBox:SetText("")
self.focusBox:SetWidth(150)
self.focusBox:Dock(BOTTOM)
self.focusBox:SetTooltip("Focus the profiling on a single function.\nEnter a global function name here (like player.GetAll)\nYou're not allowed to call functions in here (e.g. hook.GetTable() is not allowed)")
function self.focusBox:OnChange()
FProfiler.UI.updateCurrentRealm("focusStr", self:GetText())
end
FProfiler.UI.onCurrentRealmUpdate("focusObj", function(new)
self.funcIndicator.color = FProfiler.UI.getCurrentRealmValue("focusStr") == "" and Color(0, 0, 0, 0) or new and Color(80, 255, 80, 255) or Color(255, 80, 80, 255)
end)
FProfiler.UI.onCurrentRealmUpdate("focusStr", function(new, old)
if self.focusBox:GetText() == new then return end
self.focusBox:SetText(tostring(new))
end)
end
function FOCUSPANEL:PerformLayout()
self.focusBox:SetWide(200)
self.focusLabel:SizeToContents()
end
derma.DefineControl("FProfileFocusPanel", "", FOCUSPANEL, "Panel")
--[[-------------------------------------------------------------------------
The timer that keeps track of for how long the profiling has been going on
---------------------------------------------------------------------------]]
local TIMERPANEL = {}
function TIMERPANEL:Init()
self:DockPadding(0, 5, 0, 5)
self:DockMargin(5, 0, 5, 0)
self.timeLabel = vgui.Create("DLabel", self)
self.timeLabel:SetDark(true)
self.timeLabel:SetText("Total profiling time:")
self.timeLabel:SizeToContents()
self.timeLabel:Dock(TOP)
self.counter = vgui.Create("DLabel", self)
self.counter:SetDark(true)
self.counter:SetText("00:00:00")
self.counter:SizeToContents()
self.counter:Dock(RIGHT)
function self.counter:Think()
local recordTime, sessionStart = FProfiler.UI.getCurrentRealmValue("recordTime"), FProfiler.UI.getCurrentRealmValue("sessionStart")
local totalTime = recordTime + (sessionStart and (CurTime() - sessionStart) or 0)
self:SetText(string.FormattedTime(totalTime, "%02i:%02i:%02i"))
end
end
function TIMERPANEL:PerformLayout()
self.timeLabel:SizeToContents()
self.counter:SizeToContents()
end
derma.DefineControl("FProfileTimerPanel", "", TIMERPANEL, "Panel")
--[[-------------------------------------------------------------------------
The top bar
---------------------------------------------------------------------------]]
local MAGICBAR = {}
function MAGICBAR:Init()
self:DockPadding(5, 5, 5, 5)
self.realmpanel = vgui.Create("FProfileRealmPanel", self)
-- (Re)Start profiling
self.restartProfiling = vgui.Create("DButton", self)
self.restartProfiling:SetText(" (Re)Start\n Profiling")
self.restartProfiling:DockMargin(0, 0, 5, 0)
self.restartProfiling:Dock(LEFT)
self.restartProfiling.DoClick = function()
FProfiler.UI.updateCurrentRealm("shouldReset", true)
FProfiler.UI.updateCurrentRealm("status", "Started")
end
FProfiler.UI.onCurrentRealmUpdate("status", function(new)
self.restartProfiling:SetDisabled(new == "Started")
end)
-- Stop profiling
self.stopProfiling = vgui.Create("DButton", self)
self.stopProfiling:SetText(" Stop\n Profiling")
self.stopProfiling:DockMargin(0, 0, 5, 0)
self.stopProfiling:Dock(LEFT)
self.stopProfiling.DoClick = function()
FProfiler.UI.updateCurrentRealm("status", "Stopped")
end
FProfiler.UI.onCurrentRealmUpdate("status", function(new)
self.stopProfiling:SetDisabled(new == "Stopped")
end)
-- Continue profiling
self.continueProfiling = vgui.Create("DButton", self)
self.continueProfiling:SetText(" Continue\n Profiling")
self.continueProfiling:DockMargin(0, 0, 5, 0)
self.continueProfiling:Dock(LEFT)
self.continueProfiling.DoClick = function()
FProfiler.UI.updateCurrentRealm("shouldReset", false)
FProfiler.UI.updateCurrentRealm("status", "Started")
end
FProfiler.UI.onCurrentRealmUpdate("status", function(new)
self.continueProfiling:SetDisabled(new == "Started")
end)
self.realmpanel:Dock(LEFT)
self.focuspanel = vgui.Create("FProfileFocusPanel", self)
self.focuspanel:Dock(LEFT)
-- Timer
self.timerpanel = vgui.Create("FProfileTimerPanel", self)
self.timerpanel:Dock(RIGHT)
end
function MAGICBAR:PerformLayout()
self.realmpanel:SizeToChildren(true, false)
self.focuspanel:SizeToChildren(true, false)
self.timerpanel:SizeToChildren(true, false)
end
derma.DefineControl("FProfileMagicBar", "", MAGICBAR, "DPanel")
--[[-------------------------------------------------------------------------
A custom sort by column function to deal with sorting by numeric value
--------------------------------------------------------------------------]]
local function SortByColumn(self, ColumnID, Desc)
table.Copy(self.Sorted, self.Lines)
table.sort(self.Sorted, function(a, b)
if Desc then
a, b = b, a
end
local aval = a:GetSortValue(ColumnID) or a:GetColumnText(ColumnID)
local bval = b:GetSortValue(ColumnID) or b:GetColumnText(ColumnID)
local anum = tonumber(aval)
local bnum = tonumber(bval)
if anum and bnum then
return anum < bnum
end
return tostring(aval) < tostring(bval)
end)
self:SetDirty(true)
self:InvalidateLayout()
end
--[[-------------------------------------------------------------------------
The Bottlenecks tab's contents
---------------------------------------------------------------------------]]
local BOTTLENECKTAB = {}
BOTTLENECKTAB.SortByColumn = SortByColumn
function BOTTLENECKTAB:Init()
self:SetMultiSelect(false)
self:AddColumn("Name")
self:AddColumn("Path")
self:AddColumn("Lines")
self:AddColumn("Amount of times called")
self:AddColumn("Total time in ms (inclusive)")
self:AddColumn("Average time in ms (inclusive)")
FProfiler.UI.onCurrentRealmUpdate("bottlenecks", function(new)
self:Clear()
for _, row in ipairs(new) do
local names = {}
local path = row.info.short_src
local lines = path ~= "[C]" and row.info.linedefined .. " - " .. row.info.lastlinedefined or "N/A"
local amountCalled = row.total_called
local totalTime = row.total_time * 100
local avgTime = row.average_time * 100
for _, fname in ipairs(row.names or {}) do
if fname.namewhat == "" and fname.name == "" then continue end
table.insert(names, fname.namewhat .. " " .. fname.name)
end
if #names == 0 then names[1] = "Unknown" end
local line = self:AddLine(table.concat(names, "/"), path, lines, amountCalled, totalTime, avgTime)
line.data = row
end
end)
FProfiler.UI.onCurrentRealmUpdate("currentSelected", function(new, old)
if new == old then return end
for _, line in pairs(self.Lines) do
line:SetSelected(line.data.func == new.func)
end
end)
end
function BOTTLENECKTAB:OnRowSelected(id, line)
FProfiler.UI.updateCurrentRealm("currentSelected", line.data)
end
derma.DefineControl("FProfileBottleNecks", "", BOTTLENECKTAB, "DListView")
--[[-------------------------------------------------------------------------
The Top n lag spikes tab's contents
---------------------------------------------------------------------------]]
local TOPTENTAB = {}
TOPTENTAB.SortByColumn = SortByColumn
function TOPTENTAB:Init()
self:SetMultiSelect(false)
self:AddColumn("Name")
self:AddColumn("Path")
self:AddColumn("Lines")
self:AddColumn("Runtime in ms")
FProfiler.UI.onCurrentRealmUpdate("topLagSpikes", function(new)
self:Clear()
for _, row in ipairs(new) do
if not row.func then break end
local name = row.info.name and row.info.name ~= "" and (row.info.namewhat .. " " .. row.info.name) or "Unknown"
local path = row.info.short_src
local lines = path ~= "[C]" and row.info.linedefined .. " - " .. row.info.lastlinedefined or "N/A"
local runtime = row.runtime * 100
local line = self:AddLine(name, path, lines, runtime)
line.data = row
end
end)
FProfiler.UI.onCurrentRealmUpdate("currentSelected", function(new, old)
if new == old then return end
for _, line in pairs(self.Lines) do
line:SetSelected(line.data.func == new.func)
end
end)
end
function TOPTENTAB:OnRowSelected(id, line)
FProfiler.UI.updateCurrentRealm("currentSelected", line.data)
end
derma.DefineControl("FProfileTopTen", "", TOPTENTAB, "DListView")
--[[-------------------------------------------------------------------------
The Tab panel of the bottlenecks and top n lag spikes
---------------------------------------------------------------------------]]
local RESULTSHEET = {}
function RESULTSHEET:Init()
self:DockMargin(0, 10, 0, 0)
self:SetPadding(0)
self.bottlenecksTab = vgui.Create("FProfileBottleNecks")
self:AddSheet("Bottlenecks", self.bottlenecksTab)
self.toptenTab = vgui.Create("FProfileTopTen")
self:AddSheet("Top 50 most expensive function calls", self.toptenTab)
end
derma.DefineControl("FProfileResultSheet", "", RESULTSHEET, "DPropertySheet")
--[[-------------------------------------------------------------------------
The function details panel
---------------------------------------------------------------------------]]
local FUNCDETAILS = {}
function FUNCDETAILS:Init()
self.titleLabel = vgui.Create("DLabel", self)
self.titleLabel:SetDark(true)
self.titleLabel:SetFont("DermaLarge")
self.titleLabel:SetText("Function Details")
self.titleLabel:SizeToContents()
-- self.titleLabel:Dock(TOP)
self.focus = vgui.Create("DButton", self)
self.focus:SetText("Focus")
self.focus:SetTall(50)
self.focus:SetFont("DermaDefaultBold")
self.focus:Dock(BOTTOM)
function self.focus:DoClick()
local sel = FProfiler.UI.getCurrentRealmValue("currentSelected")
if not sel then return end
FProfiler.UI.updateCurrentRealm("focusStr", sel.func)
end
self.source = vgui.Create("DTextEntry", self)
self.source:SetKeyboardInputEnabled(false)
self.source:DockMargin(0, 40, 0, 0)
self.source:SetMultiline(true)
self.source:Dock(FILL)
FProfiler.UI.onCurrentRealmUpdate("sourceText", function(new)
self.source:SetText(string.Replace(new, "\t", " "))
end)
self.toConsole = vgui.Create("DButton", self)
self.toConsole:SetText("Print Details to Console")
self.toConsole:SetTall(50)
self.toConsole:SetFont("DermaDefaultBold")
self.toConsole:Dock(BOTTOM)
function self.toConsole:DoClick()
FProfiler.UI.updateCurrentRealm("toConsole", FProfiler.UI.getCurrentRealmValue("currentSelected"))
end
end
function FUNCDETAILS:PerformLayout()
self.titleLabel:CenterHorizontal()
end
derma.DefineControl("FProfileFuncDetails", "", FUNCDETAILS, "DPanel")
--[[-------------------------------------------------------------------------
The actual frame
---------------------------------------------------------------------------]]
local FRAME = {}
local frameInstance
function FRAME:Init()
self:SetTitle("FProfiler profiling tool")
self:SetSize(ScrW() * 0.8, ScrH() * 0.8)
self:Center()
self:SetVisible(true)
self:MakePopup()
self:SetDeleteOnClose(false)
self.magicbar = vgui.Create("FProfileMagicBar", self)
self.magicbar:SetTall(math.max(self:GetTall() * 0.07, 48))
self.magicbar:Dock(TOP)
self.resultsheet = vgui.Create("FProfileResultSheet", self)
self.resultsheet:SetWide(self:GetWide() * 0.8)
self.resultsheet:Dock(LEFT)
self.details = vgui.Create("FProfileFuncDetails", self)
self.details:SetWide(self:GetWide() * 0.2 - 12)
self.details:DockMargin(5, 31, 0, 0)
self.details:Dock(RIGHT)
end
function FRAME:OnClose()
FProfiler.UI.updateModel("frameVisible", false)
end
derma.DefineControl("FProfileFrame", "", FRAME, "DFrame")
--[[-------------------------------------------------------------------------
The command to start the profiler
---------------------------------------------------------------------------]]
concommand.Add("FProfiler",
function()
frameInstance = frameInstance or vgui.Create("FProfileFrame")
frameInstance:SetVisible(true)
FProfiler.UI.updateModel("frameVisible", true)
end,
nil, "Starts FProfiler")

View File

@@ -0,0 +1,188 @@
--[[
| This file was obtained through the combined efforts
| of Madbluntz & Plymouth Antiquarian Society.
|
| Credits: lifestorm, Gregory Wayne Rossel JR.,
| Maloy, DrPepper10 @ RIP, Atle!
|
| Visit for more: https://plymouth.thetwilightzone.ru/
--]]
--[[-------------------------------------------------------------------------
The model describes the data that the drives the UI
Loosely based on the Elm architecture
---------------------------------------------------------------------------]]
local model =
{
realm = "client", -- "client" or "server"
serverAccess = false, -- Whether the player has access to profile the server
frameVisible = false, -- Whether the frame is visible
client = {
status = "Stopped", -- Started or Stopped
shouldReset = true, -- Whether profiling should start anew
recordTime = 0, -- Total time spent on the last full profiling session
sessionStart = nil, -- When the last profiling session was started
sessionStartSysTime = nil, -- When the last profiling session was started, measured in SysTime
bottlenecks = {}, -- The list of bottleneck functions
topLagSpikes = {}, -- Top of lagging functions
currentSelected = nil, -- Currently selected function
focusObj = nil, -- The current function being focussed upon in profiling
focusStr = "", -- The current function name being entered
toConsole = nil, -- Any functions that should be printed to console
sourceText = "", -- The text of the source function (if available)
},
server = {
status = "Stopped", -- Started or Stopped
shouldReset = true, -- Whether profiling should start anew
bottlenecks = {}, -- The list of bottleneck functions
recordTime = 0, -- Total time spent on the last full profiling session
sessionStart = nil, -- When the last profiling session was started
topLagSpikes = {}, -- Top of lagging functions
currentSelected = nil, -- Currently selected function
focusObj = nil, -- The current function being focussed upon in profiling
focusStr = "", -- The current function name
toConsole = nil, -- Any functions that should be printed to console
sourceText = "", -- The text of the source function (if available)
fromServer = false, -- Whether a change of the model came from the server.
},
}
local updaters = {}
--[[-------------------------------------------------------------------------
Update the model.
Automatically calls the registered update hook functions
e.g. updating the realm would be:
FProfiler.UI.updateModel("realm", "server")
---------------------------------------------------------------------------]]
function FProfiler.UI.updateModel(path, value)
path = istable(path) and path or {path}
local updTbl = updaters
local mdlTbl = model
local key = path[#path]
for i = 1, #path - 1 do
mdlTbl = mdlTbl[path[i]]
updTbl = updTbl and updTbl[path[i]]
end
local oldValue = mdlTbl[key]
mdlTbl[key] = value
for _, updFunc in ipairs(updTbl and updTbl[key] or {}) do
updFunc(value, oldValue)
end
end
--[[-------------------------------------------------------------------------
Update the model of the current realm
---------------------------------------------------------------------------]]
function FProfiler.UI.updateCurrentRealm(path, value)
path = istable(path) and path or {path}
table.insert(path, 1, model.realm)
FProfiler.UI.updateModel(path, value)
end
--[[-------------------------------------------------------------------------
Retrieve a value of the model
---------------------------------------------------------------------------]]
function FProfiler.UI.getModelValue(path)
path = istable(path) and path or {path}
local mdlTbl = model
local key = path[#path]
for i = 1, #path - 1 do
mdlTbl = mdlTbl[path[i]]
end
return mdlTbl[key]
end
--[[-------------------------------------------------------------------------
Retrieve a value of the model regardless of realm
---------------------------------------------------------------------------]]
function FProfiler.UI.getCurrentRealmValue(path)
path = istable(path) and path or {path}
table.insert(path, 1, model.realm)
return FProfiler.UI.getModelValue(path)
end
--[[-------------------------------------------------------------------------
Registers a hook that gets triggered when a certain part of the model is updated
e.g. FProfiler.UI.onModelUpdate("realm", print) prints when the realm is changed
---------------------------------------------------------------------------]]
function FProfiler.UI.onModelUpdate(path, func)
path = istable(path) and path or {path}
local updTbl = updaters
local mdlTbl = model
local key = path[#path]
for i = 1, #path - 1 do
mdlTbl = mdlTbl[path[i]]
updTbl[path[i]] = updTbl[path[i]] or {}
updTbl = updTbl[path[i]]
end
updTbl[key] = updTbl[key] or {}
table.insert(updTbl[key], func)
-- Call update with the initial value
if mdlTbl[key] ~= nil then
func(mdlTbl[key], mdlTbl[key])
end
end
--[[-------------------------------------------------------------------------
Registers a hook to both realms
---------------------------------------------------------------------------]]
function FProfiler.UI.onCurrentRealmUpdate(path, func)
path = istable(path) and path or {path}
table.insert(path, 1, "client")
FProfiler.UI.onModelUpdate(path, function(...)
if FProfiler.UI.getModelValue("realm") == "server" then return end
func(...)
end)
path[1] = "server"
FProfiler.UI.onModelUpdate(path, function(...)
if FProfiler.UI.getModelValue("realm") == "client" then return end
func(...)
end)
end
--[[-------------------------------------------------------------------------
When the realm is changed, all update functions of the new realm are to be called
---------------------------------------------------------------------------]]
FProfiler.UI.onModelUpdate("realm", function(new, old)
if not updaters[new] then return end
for k, funcTbl in pairs(updaters[new]) do
for _, func in ipairs(funcTbl) do
func(model[new][k], model[new][k])
end
end
end)

View File

@@ -0,0 +1,305 @@
--[[
| This file was obtained through the combined efforts
| of Madbluntz & Plymouth Antiquarian Society.
|
| Credits: lifestorm, Gregory Wayne Rossel JR.,
| Maloy, DrPepper10 @ RIP, Atle!
|
| Visit for more: https://plymouth.thetwilightzone.ru/
--]]
--[[-------------------------------------------------------------------------
The server is involved in the ui in the sense that it interacts with its model
---------------------------------------------------------------------------]]
-- Net messages
util.AddNetworkString("FProfile_startProfiling")
util.AddNetworkString("FProfile_stopProfiling")
util.AddNetworkString("FProfile_focusObj")
util.AddNetworkString("FProfile_getSource")
util.AddNetworkString("FProfile_printFunction")
util.AddNetworkString("FProfile_fullModelUpdate")
util.AddNetworkString("FProfile_focusUpdate")
util.AddNetworkString("FProfile_unsubscribe")
--[[-------------------------------------------------------------------------
Simplified version of the model
Contains only what the server needs to know
---------------------------------------------------------------------------]]
local model =
{
focusObj = nil, -- the function currently in focus
sessionStart = nil, -- When the last profiling session was started. Used for the live timer.
sessionStartSysTime = nil, -- Same as sessionStart, but measured in SysTime. Used to update recordTime
recordTime = 0, -- Total time spent on the last full profiling session
bottlenecks = {}, -- The list of bottleneck functions
topLagSpikes = {}, -- Top of lagging functions
subscribers = RecipientFilter(), -- The players that get updates of the profiler model
}
--[[-------------------------------------------------------------------------
Helper function: receive a net message
---------------------------------------------------------------------------]]
local function receive(msg, f)
net.Receive(msg, function(len, ply)
-- Check access.
CAMI.PlayerHasAccess(ply, "FProfiler", function(b, _)
if not b then return end
f(len, ply)
end)
end)
end
--[[-------------------------------------------------------------------------
Helper function:
Write generic row data to a net message
---------------------------------------------------------------------------]]
local function writeRowData(row)
net.WriteString(tostring(row.func))
net.WriteString(row.info.short_src)
net.WriteUInt(row.info.linedefined, 16)
net.WriteUInt(row.info.lastlinedefined, 16)
end
--[[-------------------------------------------------------------------------
Helper function:
Send the bottlenecks to the client
Only sends the things displayed
---------------------------------------------------------------------------]]
local function writeBottleNecks()
net.WriteUInt(#model.bottlenecks, 16)
for i, row in ipairs(model.bottlenecks) do
writeRowData(row)
net.WriteUInt(#row.names, 8)
for j, name in ipairs(row.names) do
net.WriteString(name.name)
net.WriteString(name.namewhat)
end
net.WriteUInt(row.total_called, 32)
net.WriteDouble(row.total_time)
net.WriteDouble(row.average_time)
end
end
--[[-------------------------------------------------------------------------
Helper function:
Sends the top n functions
---------------------------------------------------------------------------]]
local function writeTopN()
local count = #model.topLagSpikes
-- All top N f
for i = count, 0, -1 do
if model.topLagSpikes and model.topLagSpikes[i] and model.topLagSpikes[i].info then break end -- Entry exists
count = i
end
net.WriteUInt(count, 8)
for i = 1, count do
local row = model.topLagSpikes[i]
if not row.info then break end
writeRowData(row)
net.WriteString(row.info.name or "")
net.WriteString(row.info.namewhat or "")
net.WriteDouble(row.runtime)
end
end
-- Start profiling
local function startProfiling()
model.sessionStart = CurTime()
model.sessionStartSysTime = SysTime()
FProfiler.Internal.start(model.focusObj)
net.Start("FProfile_startProfiling")
net.WriteDouble(model.recordTime)
net.WriteDouble(model.sessionStart)
net.Send(model.subscribers:GetPlayers())
end
-- Stop profiling
local function stopProfiling()
FProfiler.Internal.stop()
model.recordTime = model.recordTime + SysTime() - (model.sessionStartSysTime or 0)
model.sessionStart = nil
model.sessionStartSysTime = nil
model.bottlenecks = FProfiler.Internal.getAggregatedResults(100)
model.topLagSpikes = FProfiler.Internal.getMostExpensiveSingleCalls()
net.Start("FProfile_stopProfiling")
net.WriteDouble(model.recordTime)
writeBottleNecks()
writeTopN()
net.Send(model.subscribers:GetPlayers())
end
--[[-------------------------------------------------------------------------
Receive an update of the function to focus on
---------------------------------------------------------------------------]]
receive("FProfile_focusObj", function(_, ply)
local funcStr = net.ReadString()
model.focusObj = FProfiler.funcNameToObj(funcStr)
net.Start("FProfile_focusObj")
net.WriteBool(model.focusObj and true or false)
net.Send(ply)
-- Send a focus update to all other players
net.Start("FProfile_focusUpdate")
net.WriteString(funcStr)
net.WriteBool(model.focusObj and true or false)
model.subscribers:RemovePlayer(ply)
net.Send(model.subscribers:GetPlayers())
model.subscribers:AddPlayer(ply)
end)
--[[-------------------------------------------------------------------------
Receive a "start profiling" signal
---------------------------------------------------------------------------]]
receive("FProfile_startProfiling", function(_, ply)
local shouldReset = net.ReadBool()
if shouldReset then
FProfiler.Internal.reset()
model.recordTime = 0
end
startProfiling()
end)
--[[-------------------------------------------------------------------------
Receive a stop profiling signal
---------------------------------------------------------------------------]]
receive("FProfile_stopProfiling", function(_, ply)
stopProfiling()
end)
--[[-------------------------------------------------------------------------
Send the source of a function to a client
---------------------------------------------------------------------------]]
receive("FProfile_getSource", function(_, ply)
local func = FProfiler.funcNameToObj(net.ReadString())
if not func then return end
local info = debug.getinfo(func)
if not info then return end
net.Start("FProfile_getSource")
net.WriteString(FProfiler.readSource(info.short_src, info.linedefined, info.lastlinedefined) or "")
net.Send(ply)
end)
--[[-------------------------------------------------------------------------
Print the details of a function
---------------------------------------------------------------------------]]
receive("FProfile_printFunction", function(_, ply)
local source = net.ReadBool() -- true is from bottlenecks, false is from Top-N
local dataSource = source and model.bottlenecks or model.topLagSpikes
local func = net.ReadString()
local data
for _, row in ipairs(dataSource or {}) do
if tostring(row.func) == func then data = row break end
end
if not data then return end
-- Show the data
show(data)
local plaintext = showStr(data)
-- Write to file if necessary
file.CreateDir("fprofiler")
file.Write("fprofiler/profiledata.txt", plaintext)
MsgC(Color(200, 200, 200), "-----", Color(120, 120, 255), "NOTE", Color(200, 200, 200), "---------------\n")
MsgC(Color(200, 200, 200), "If the above function does not fit in console, you can find it in data/fprofiler/profiledata.txt\n\n")
-- Listen server hosts already see the server console
if ply:IsListenServerHost() then return end
-- Send a plaintext version to the client
local binary = util.Compress(plaintext)
net.Start("FProfile_printFunction")
net.WriteData(binary, #binary)
net.Send(ply)
end)
--[[-------------------------------------------------------------------------
Request of a full model update
Particularly useful when someone else has done (or is performing) a profiling session
and the current player wants to see the results
---------------------------------------------------------------------------]]
receive("FProfile_fullModelUpdate", function(_, ply)
-- This player is now subscribed to the updates
model.subscribers:AddPlayer(ply)
net.Start("FProfile_fullModelUpdate")
net.WriteBool(model.focusObj ~= nil)
if model.focusObj ~= nil then net.WriteString(tostring(model.focusObj)) end
-- Bool also indicates whether it's currently profiling
net.WriteBool(model.sessionStart ~= nil)
if model.sessionStart ~= nil then net.WriteDouble(model.sessionStart) end
net.WriteDouble(model.recordTime)
writeBottleNecks()
writeTopN()
net.Send(ply)
end)
--[[-------------------------------------------------------------------------
Unsubscribe from the updates of the profiler
---------------------------------------------------------------------------]]
receive("FProfile_unsubscribe", function(_, ply)
model.subscribers:RemovePlayer(ply)
end)
--[[-------------------------------------------------------------------------
API function: start profiling
---------------------------------------------------------------------------]]
function FProfiler.start(focus)
FProfiler.Internal.reset()
model.recordTime = 0
model.focusObj = focus
startProfiling()
end
--[[-------------------------------------------------------------------------
API function: stop profiling
---------------------------------------------------------------------------]]
function FProfiler.stop()
stopProfiling()
end
--[[-------------------------------------------------------------------------
API function: continue profiling
---------------------------------------------------------------------------]]
function FProfiler.continueProfiling()
startProfiling()
end

View File

@@ -0,0 +1,238 @@
--[[
| This file was obtained through the combined efforts
| of Madbluntz & Plymouth Antiquarian Society.
|
| Credits: lifestorm, Gregory Wayne Rossel JR.,
| Maloy, DrPepper10 @ RIP, Atle!
|
| Visit for more: https://plymouth.thetwilightzone.ru/
--]]
local get, update, onUpdate = FProfiler.UI.getModelValue, FProfiler.UI.updateModel, FProfiler.UI.onModelUpdate
--[[-------------------------------------------------------------------------
Update the current selected focus object when data is entered
---------------------------------------------------------------------------]]
onUpdate({"server", "focusStr"}, function(new)
if not new or get({"server", "fromServer"}) then return end
net.Start("FProfile_focusObj")
net.WriteString(new)
net.SendToServer()
end)
net.Receive("FProfile_focusObj", function()
update({"server", "focusObj"}, net.ReadBool() and get({"server", "focusStr"}) or nil)
end)
-- A focus update occurs when someone else changes the focus
net.Receive("FProfile_focusUpdate", function()
update({"server", "fromServer"}, true)
local focusStr = net.ReadString()
update({"server", "focusStr"}, focusStr)
update({"server", "focusObj"}, net.ReadBool() and focusStr or nil)
update({"server", "fromServer"}, false)
end)
--[[-------------------------------------------------------------------------
(Re)start profiling
---------------------------------------------------------------------------]]
local function restartProfiling()
local shouldReset = get({"server", "shouldReset"})
net.Start("FProfile_startProfiling")
net.WriteBool(shouldReset)
net.SendToServer()
end
net.Receive("FProfile_startProfiling", function()
update({"server", "fromServer"}, true)
update({"server", "status"}, "Started")
update({"server", "recordTime"}, net.ReadDouble())
update({"server", "sessionStart"}, net.ReadDouble())
update({"server", "fromServer"}, false)
end)
--[[-------------------------------------------------------------------------
Stop profiling
---------------------------------------------------------------------------]]
local function stopProfiling()
net.Start("FProfile_stopProfiling")
net.SendToServer()
end
-- Read a row from a net message
local function readDataRow(countSize, readSpecific)
local res = {}
local count = net.ReadUInt(countSize)
for i = 1, count do
local row = {}
row.info = {}
row.func = net.ReadString()
row.info.short_src = net.ReadString()
row.info.linedefined = net.ReadUInt(16)
row.info.lastlinedefined = net.ReadUInt(16)
readSpecific(row)
table.insert(res, row)
end
return res
end
-- Read a bottleneck row
local function readBottleneckRow(row)
local nameCount = net.ReadUInt(8)
row.names = {}
for i = 1, nameCount do
table.insert(row.names, {
name = net.ReadString(),
namewhat = net.ReadString()
})
end
row.total_called = net.ReadUInt(32)
row.total_time = net.ReadDouble()
row.average_time = net.ReadDouble()
end
-- Read the top n row
local function readTopNRow(row)
row.info.name = net.ReadString()
row.info.namewhat = net.ReadString()
row.runtime = net.ReadDouble()
end
net.Receive("FProfile_stopProfiling", function()
update({"server", "fromServer"}, true)
update({"server", "status"}, "Stopped")
update({"server", "sessionStart"}, nil)
update({"server", "recordTime"}, net.ReadDouble())
update({"server", "bottlenecks"}, readDataRow(16, readBottleneckRow))
update({"server", "topLagSpikes"}, readDataRow(8, readTopNRow))
update({"server", "fromServer"}, false)
end)
--[[-------------------------------------------------------------------------
Start/stop recording when the recording status is changed
---------------------------------------------------------------------------]]
onUpdate({"server", "status"}, function(new, old)
if new == old or get({"server", "fromServer"}) then return end
(new == "Started" and restartProfiling or stopProfiling)()
end)
--[[-------------------------------------------------------------------------
Update info when a different line is selected
---------------------------------------------------------------------------]]
onUpdate({"server", "currentSelected"}, function(new)
if not new or not new.info or not new.info.linedefined or not new.info.lastlinedefined or not new.info.short_src then return end
net.Start("FProfile_getSource")
net.WriteString(tostring(new.func))
net.SendToServer()
end)
net.Receive("FProfile_getSource", function()
update({"server", "sourceText"}, net.ReadString())
end)
--[[-------------------------------------------------------------------------
When a function is to be printed to console
---------------------------------------------------------------------------]]
onUpdate({"server", "toConsole"}, function(data)
if not data then return end
update({"server", "toConsole"}, nil)
net.Start("FProfile_printFunction")
net.WriteBool(data.total_called and true or false) -- true for bottleneck function, false for top-n function
net.WriteString(tostring(data.func))
net.SendToServer()
end)
net.Receive("FProfile_printFunction", function(len)
local data = net.ReadData(len)
local decompressed = util.Decompress(data)
-- Print the text line by line, otherwise big parts of big data will not be printed
local split = string.Explode("\n", decompressed, false)
for _, line in ipairs(split) do
MsgN(line)
end
-- Write the thing to a file
file.CreateDir("fprofiler")
file.Write("fprofiler/profiledata.txt", showStr(data))
MsgC(Color(200, 200, 200), "-----", Color(120, 120, 255), "NOTE", Color(200, 200, 200), "---------------\n")
MsgC(Color(200, 200, 200), "In the server's console you can find a colour coded version of the above output.\nIf the above function does not fit in console, you can find it in data/fprofiler/profiledata.txt\n\n")
end)
--[[-------------------------------------------------------------------------
Check access when the frame opens
Also request a full serverside model update
---------------------------------------------------------------------------]]
onUpdate("frameVisible", function(isOpen)
-- Don't network if the server doesn't have FProfiler installed
if util.NetworkStringToID("FProfile_fullModelUpdate") == 0 then
update("serverAccess", false)
return
end
-- Update access
CAMI.PlayerHasAccess(LocalPlayer(), "FProfiler", function(b, _)
update("serverAccess", b)
end)
if not isOpen then
net.Start("FProfile_unsubscribe")
net.SendToServer()
return
end
net.Start("FProfile_fullModelUpdate")
net.SendToServer()
end)
net.Receive("FProfile_fullModelUpdate", function()
update({"server", "fromServer"}, true)
local focusExists = net.ReadBool()
if focusExists then
local focus = net.ReadString()
update({"server", "focusObj"}, focus)
update({"server", "focusStr"}, focus)
end
local startingTimeExists = net.ReadBool()
if startingTimeExists then
update({"server", "status"}, "Started")
update({"server", "sessionStart"}, net.ReadDouble())
else
update({"server", "status"}, "Stopped")
end
update({"server", "recordTime"}, net.ReadDouble())
update({"server", "bottlenecks"}, readDataRow(16, readBottleneckRow))
update({"server", "topLagSpikes"}, readDataRow(8, readTopNRow))
update({"server", "fromServer"}, false)
end)

View File

@@ -0,0 +1,52 @@
--[[
| This file was obtained through the combined efforts
| of Madbluntz & Plymouth Antiquarian Society.
|
| Credits: lifestorm, Gregory Wayne Rossel JR.,
| Maloy, DrPepper10 @ RIP, Atle!
|
| Visit for more: https://plymouth.thetwilightzone.ru/
--]]
-- Try to find the function represented by a string
function FProfiler.funcNameToObj(str)
if isfunction(str) then return str end
local times = FProfiler.Internal.getCallCounts()
for func, _ in pairs(times) do
if tostring(func) == str then return func end
end
local tbl = _G
local exploded = string.Explode(".", str, false)
if not exploded or not exploded[1] then return end
for i = 1, #exploded - 1 do
tbl = (tbl or {})[exploded[i]]
if not istable(tbl) then return end
end
local func = (tbl or {})[exploded[#exploded]]
if not isfunction(func) then return end
return func
end
-- Read a file
function FProfiler.readSource(fname, startLine, endLine)
if not file.Exists(fname, "GAME") then return "" end
if startLine < 0 or endLine < 0 or endLine < startLine then return "" end
local f = file.Open(fname, "r", "GAME")
for i = 1, startLine - 1 do f:ReadLine() end
local res = {}
for i = startLine, endLine do
table.insert(res, f:ReadLine() or "")
end
return table.concat(res, "\n")
end