I'm not gonna lie. I love writing my own stuff. Even if
it's shitty or bad. I, Like, Programming. Especially
useful stuff.
And that's where Fronty comes in handy.
Ramble about why all of the other static site generators are too much for me (Click this text)
I have inherently nothing against other static
site generators. I've used such generators
myself.
Hugo, for example. It's a fine generator, and has a
vast array of templates and styles. It even has
a very extensive templating language.
But it has too many bells and whistles for me.
And I would either need to manually make a
template, which would mean I have to learn the
templating language, or go with a template and
tamper with countless other files and folders,
just so I can implement my own element on the
site.
Instead of using someone else's static site generator, I could just make my own, all the while using my own templating language. How hard can it be?
[Insert whacky image of someone pointing back at another person, and asking "How do we tell him?"]
Let me show you a snippet of the template.
MainTitle() // First, we define a template name.
<h1>My Cool Place<h1> // Second, we write the HTML that we want included
MainTitle // Third, we end the template
Then we just use the template like so:
<div class="main-page">
MainTitle()
...
Easy peasy.
The implementation, however, isn't so easy peasy. The
code right now is so ugly that I wouldn't want my worst
enemy to work on it. I gotta refactor it at some point.
But it is kinda fast. ~22µs at the
fastest re-build, and 900µs at the slowest.
Most of the time is spend reading the HTML files, so
generation times are highly depending on storage speed
and storage scheduling.
Features
Can't believe I forgot about features lol.
Supported file types:
- .html
- .css
- .fnty
When running Fronty, it will create a map of all
directories in the source directory of your site, and
re-create it in a build folder. Doing re-build, Fronty
will replace all template names with the associated HTML
defined in any
.fnty files.
Pretty simple. It's just search/replace. Now you might
be asking, "Why not use RegEx?!?"
I don't like RegEx. Yep. I said it. I don't like RegEx.
One must not forget this legendary StackOverflow
comment.
Benchmarks
If you know me in real life, you know I like to spam
benchmarks in our DMs. Almost to a religious degree.
When making programming projects, I like to first make a
"naive" implementation that just works and is intuitive.
Then I bring in a benchmarking framework, in this case
BenchmarkDotNet, and start working on different
approaches to the naive implementation.
This is a small list of implementations that produces
the same HTML after a build:
- Builder.cs
- SpanBuilder.cs
- ReworkedPathBuilder.cs
- DynamicFileBuilder.cs
- FastFileWriterBuilder.cs
- I did also have an AsyncBuilder.cs implementation, but it was so embarrassingly slow, that I decided to not go further with the idea.
"Quick" info about why it was slower (Click this text)
TLDR: Async doesn't make sense for my use case.
When making anything resembling a web API or a
server of sorts, and you have to load anything
from a file system, or just any IO operation in
general, it can be beneficial to mark
that operation as async, as the program can then
fetch the data and process other data at
the same time.
It feels like multi threading without actually
being multi threading. Or something like that.
In reality, it kind of depends on what your
program does. If it is a web server, then
yes, making the loading async is for the best,
as a method can hand over I/O to the operating
system, return to caller, and resume at a later
time, when the operating system has retrieved
the data at a later time, all the while the
thread can run other work while we wait.
Please be aware that async, at least in C#, does
not imply multi threading. So if you have
multiple async methods called from the same
thread, they will not run at the same time.
Although they might return on a different thread
than they started on, they will not run in
parallel, as they are async, not parallel.
Parallelism means that methods are doing the
work at the same time.
Async programming is not true multi threading,
but it will feel like it. So when I made
all of my methods async, and called the async
overloaded methods for file-handling, what I
really did was making my application wait for
the data to load, while I could in theory do
something else. While that is ideal, what really
ended up happening was that I implicitly created
additional state-machines and even more memory
allocations.
All my homies hate extra allocations.
See, the thing with async in C# is that it
creates a state-machine, for when a method is
allowed to run, or is to be paused. That needs
additional memory to keep track of, and creates
overhead of having to handle pausing and
starting and checking if the method is running.
That's wasteful for what I need, as everything
is already running on the same thread,
synchronously. I take data from a file, I
process that data, I save that processed data.
There is nothing else. I don't need
interactivity, and I don't need to do multiple
things in the same thread, as there is nothing
else to do in the same thread. There is no other
data to pull from at this moment in time.
Couldn't you just spawn a thread for each file
to read?
Yeah, I could. But I still wouldn't need to use
the async overloads, as we're now executing
reading each file at the same time. So it
wouldn't really matter all that much if they
were async or not, as each work would still be
blocked as they perform their own I/O work.
End of async rant.
Anyway, here are some benchmark results.
| Method | Mean | StdDev | Rank | Gen0 | Gen1 | Allocated |
|---|---|---|---|---|---|---|
| FastFileWriter | 273.4 µs | 0.75 µs | 1 | 34.1797 | 4.3945 | 282.01 KB |
| DynamicFileBuilder | 294.1 µs | 1.20 µs | 2 | 44.4336 | 4.3945 | 366.24 KB |
| ReworkedPathBuilder | 508.7 µs | 11.53 µs | 3 | 28.3203 | 0.9766 | 237.87 KB |
| SpanBuilder | 563.0 µs | 9.03 µs | 4 | 36.1328 | 3.9063 | 296.55 KB |
| Builder | 682.7 µs | 34.35 µs | 5 | 79.1016 | 7.8125 | 649.33 KB |
Numbers explanation:
- Mean: Arithmetic mean of all measurements
- StdDev: Standard deviation of all measurements
- Rank: Relative position of current benchmark mean among all benchmarks (Arabic style)
- Gen0: GC Generation 0 collects per 1000 operations
- Gen1: GC Generation 1 collects per 1000 operations
- Allocated: Allocated memory per single operation (managed only, inclusive, 1KB = 1024B)
Conclusion
I'm a bit sadden that the fastest method uses that much
memory. I had hoped it would use less, but you can't win
every time. I could potentially squeeze more speed out
of it by implementing a custom reader. But I think this
is fine enough. I could re-write it in a lower-level
language, but I don't think I will lol. I've already
spent enough time making it this fast, and honestly, I
like how the code works and looks.
I can finally sleep again.
If you want, you can check out the code for Fronty on my
git
right here:
https://git.foxingitup.com/foxingitup/Fronty