Sr. Content Developer at Microsoft, working remotely in PA, TechBash conference organizer, former Microsoft MVP, Husband, Dad and Geek.
159970 stories
·
33 followers

Try Azure SRE Agent with no always-on charges

1 Share

We are happy to announce a 30-day trial experience for Azure SRE Agent. New customers can create and configure the SRE Agent at their own pace, with no charges for setup time or keeping agents ready. During the trial, you can connect your agents to telemetry, source code, incident management platforms, and other operational tools, and pay only for the Azure Agent Units (AAUs) consumed when your agents perform work.

We are also excited to share the GA of Azure SRE Agent VNet integration and the Public Preview of Live Reports. With VNet support, the agent operates under your existing network controls, so it can reach private resources during an incident without opening your network boundary. Live Reports let you create operational views once and keep them fresh with live data every time your team opens them.

Start your 30-day trial at https://sre.azure.com. Create your first SRE Agent, connect it to the systems your team relies on, and build a live report for the operational view your team checks every morning.

Learn more about Trial access, VNET and Live Reports details of Azure SRE Agent.

Why use Azure SRE Agent?

When you build or maintain large scale production systems, you know how much time disappears into gathering signals, correlating alerts with recent changes, and repeating the same investigation steps. Azure SRE Agent brings together telemetry, source code, Azure resources, and your operational knowledge to help your team move from reactive troubleshooting to proactive, automated operations, reducing the time from analysis to action from days and hours to minutes.

Azure SRE Agent has scaled to over 3,000 internal Microsoft services, processing more than 1.5 million incidents with up to 50% resolved autonomously without human intervention. Customers like Zafin, Provation, and InEight are seeing similar impact, with InEight reporting 80% reduction in incident investigation time and 80% reduction in build failure triage. Read how these organizations are transforming cloud operations in A paradigm shift in cloud operations with Azure SRE Agent.

What’s new in this announcement

30-day trial with no always-on charges — New customers can create up to 3 agents. Each agent has its own 30-day trial period. The baseline always-on charges are waived during that time. Active usage (AAU) charges apply when the agent performs work. See pricing and billing for details, including what happens when the trial ends.

VNet support (GA) — With VNET support, the agent routes outbound traffic through a delegated subnet in your virtual network, with your NSG rules, private DNS, and firewall applied. It reaches databases behind private endpoints, internal services, and monitoring stores without requiring you to open your network boundary. Learn more about VNET support here.

Live Reports (public preview) — Ops teams answer the same questions from the same data sources every day, often across scattered dashboards and scripts. Live Reports let you create a report from a chat conversation, and it pulls fresh data from your connected systems each time it is opened. The layout stays fixed; the data stays current. Read the full blog: Introducing Live Reports

Where to start

Do not start with a broad “manage everything” goal. Pick one service and one problem you can recognize and measure. A familiar incident is often the best first test because you already know what good output looks like.

Here are a few practical ways developers, SREs, and IT operations teams can use the 30-day trial:

  • Investigate a production issue: Let the agent work across alerts, logs, metrics, traces, Azure resource state, code, and recent deployments to build a likely root cause and suggest a mitigation.
  • Automate alert response: Trigger investigations from Azure Monitor, Datadog, Dynatrace, or PagerDuty and guide the response with your team’s existing runbooks and practices.
  • Get ahead of operational risks: Run post-deployment health checks, monitor certificate expiration, detect configuration drift, review costs, or support compliance checks.
  • Close the loop: Have your agent send evidence, root cause, mitigation, and follow-up work to GitHub, ServiceNow, Jira, or Azure DevOps.
  • Build a Live Report: Replace the operational view your team rebuilds by hand each morning with a report that refreshes on open.

Get started today

Create your first Azure SRE Agent, connect it to your environment, and start with one real source of operational toil: Create your first Azure SRE Agent

Additional Resources: Getting Started | Pricing and Billing | Azure SRE Agent Blogs | Hands-on-lab | Quick start templates

 

The post Try Azure SRE Agent with no always-on charges appeared first on Microsoft for Developers.

Read the whole story
alvinashcraft
just a second ago
reply
Pennsylvania, USA
Share this story
Delete

When to Use ListView vs. DataGrid in Real-World Projects

1 Share

Depending on the type of user, the number of fields and the tasks being performed, does your Blazor app need a DataGrid or a ListView?

Displaying data to users is common in web applications. One question you may ask, and which doesn’t always have the same answer, is how to choose the right component to do it.

Would it be appropriate to show a table with all the columns? Or would a card gallery like those used in mobile apps be better?

For that reason, in this article we will analyze the components Blazor ListView and Blazor DataGrid from the Progress Telerik UI library, comparing their features. Let’s go!

The Problem of Choosing the Wrong Component

How we choose to display information in our web application depends on several variables:

  • The type of user: A user who requires detailed information about an item is not the same as someone who only needs to see general information.
  • The amount of fields to display: Quickly viewing information is not the same as analyzing and comparing data.
  • The task to perform with the data: You may need to perform complex operations like grouping, applying filters or making comparisons between them, or you may not.

Not being clear about the purpose of using the information could lead to poor user experiences or make it difficult to work with the data.

Getting to Know the Blazor DataGrid Component

The Blazor DataGrid component allows viewing data in a tabular form. The idea of the component is to display many records in a table format with all the necessary tools for a user to analyze them.

It is a quite robust and flexible component, which in its latest versions incorporates AI features for better data understanding. Among its main capabilities we can find:

  • Sorting by one or multiple columns
  • Filtering on each column
  • Grouping by dragging headers
  • Paging with traditional pagination or virtual scrolling
  • Inline or popup editing of records
  • Resizable, reorderable and lockable columns
  • Exporting to Excel and PDF
  • Single or multiple selection
  • Customization using templates
  • Drag & drop support
  • Smart AI features that work with natural language queries

From the characteristics described, you can notice that we are talking about a component that should be used when users want to see many rows and fields at the same time and be able to manipulate them.

Some ideal use cases for this component are administrative panels, ERPs, support dashboards, internal tools or any interface where data takes priority over aesthetics.

The Blazor ListView Component

The Blazor ListView component can be considered a freer component than the Blazor Grid. Instead of a tabular structure, the data is provided to you as a context that you can render as you wish. This means you can customize the UI to create cards, timelines, custom rows or any Razor structure you want.

Some of the component’s main features include:

  • Customizable built-in paging
  • On-demand loading
  • Handling of CUD operations through built-in commands
  • Custom data source operations
  • Fully customizable templates

This component is ideal when the data is not tabular or when the layout needs to adapt to multiple screen styles. Some examples of when to use this component are news feeds, visual catalogs, item galleries, among others.

When to Use a Grid vs. a ListView?

In addition to the analysis we’ve done for each component, I’d like to share some comparative points about when to use a Grid and when to use a ListView.

It’s advisable to use a Grid when:

  • You need the user to be able to compare values across rows
  • Each record has many fields to be analyzed
  • Screen space is not an issue
  • You require complex operations such as sorting, filtering or grouping
  • You need to edit data in bulk
  • You need to export table data

On the other hand, it’s advisable to use a ListView when:

  • Records have few fields or are ideal for visually rich interfaces
  • User interaction involves viewing items one at a time, not analyzing them
  • You need full control over the layout of each item
  • You are building some type of app that includes a card-like component or similar

Let’s see how both views look in a real example.

Creating a Blazor Project with ListView and Grid

To demonstrate the difference between the two components and see when it’s convenient to use one over the other, let’s create a component that loads 50 orders and allows switching the view between a ListView-style and a Grid-style.

To achieve this, start by creating a project with the Blazor Web App template, selecting Interactive render mode in Server and Interactivity location in Global. Then, you can follow the official installation guide for Telerik UI for Blazor to install the Telerik components in your project.

Defining the App Data Model

Once the Telerik components are installed in the project, the next step will be to create the data model that represents a sales order in the system. For this, we will use a record type as follows:

public record SalesOrder(
    int OrderId,
    string Customer,
    string Product,
    string Category,
    int Quantity,
    decimal UnitPrice,
    DateTime OrderDate,
    string Status)
{
    public decimal Total => Quantity * UnitPrice;
}

Now, let’s create a service that will generate the fictitious orders.

Creating a Data Service

Let’s create a service class that will be used to generate the fictitious orders. For this, let’s add an interface and its corresponding implementation:

public interface ISalesService
{
    IReadOnlyList<SalesOrder> GetOrders();
}

public class SalesService : ISalesService
{
    private static readonly string[] Customers =
    [
        "Contoso", "Fabrikam", "Adventure Works",
        "Northwind Traders", "Telerik"
    ];

    private static readonly (string Product, string Category, decimal Price)[] Catalog =
    [
        ("Laptop Pro 15",      "Electronics", 1299.99m),
        ("Wireless Mouse",     "Electronics",   45.00m),
        ("4K Monitor",         "Electronics",  399.50m),
        ("Mechanical Keyboard","Electronics",  129.99m),
        ("Running Shoes",      "Clothing",      89.95m),
        ("Winter Jacket",      "Clothing",     159.99m),
        ("Organic Coffee",     "Food",          24.50m),
        ("Premium Tea Box",    "Food",          18.75m),
        ("Office Chair",       "Furniture",    249.00m),
        ("Standing Desk",      "Furniture",    599.00m)
    ];

    private static readonly string[] Statuses =
        ["Pending", "Shipped", "Delivered", "Cancelled"];

    public IReadOnlyList<SalesOrder> GetOrders()
    {
        var random = new Random(42);
        var orders = new List<SalesOrder>();

        for (int i = 1; i <= 50; i++)
        {
            var item = Catalog[random.Next(Catalog.Length)];
            orders.Add(new SalesOrder(
                OrderId: 1000 + i,
                Customer: Customers[random.Next(Customers.Length)],
                Product: item.Product,
                Category: item.Category,
                Quantity: random.Next(1, 10),
                UnitPrice: item.Price,
                OrderDate: DateTime.Today.AddDays(-random.Next(0, 60)),
                Status: Statuses[random.Next(Statuses.Length)]
            ));
        }

        return orders;
    }
}

The code is simple, defining arrays with information that will simulate a business dataset, as well as generating 50 orders by randomly combining the arrays.

To be able to inject the service, we will go to Program.cs, where we will add it as Singleton:

var builder = WebApplication.CreateBuilder(args);
...
builder.Services.AddSingleton<ISalesService, SalesService>();

var app = builder.Build();

With the service ready, we can start with the visual tests.

Preparing the Orders Page

In the Components\Pages folder we will create a new component called SalesOrders.razor, which will look as follows:

@page "/sales-orders"
@using SalesOrderListGridDemo.Services
@using SalesOrderListGridDemo.Models
@rendermode InteractiveServer
@inject ISalesService SalesService

<PageTitle>Sales Orders</PageTitle>

<h1>Sales Orders</h1>

<p>
    Switch between the <strong>ListView</strong> and the
    <strong>DataGrid</strong> using the toggle below.
</p>

<div class="sales-toolbar">
    <TelerikButtonGroup SelectionMode="@ButtonGroupSelectionMode.Single">
        <ButtonGroupToggleButton Selected="@(currentView == ViewMode.ListView)"
                                 SelectedChanged="@(_ => SetView(ViewMode.ListView))">
            ListView
        </ButtonGroupToggleButton>
        <ButtonGroupToggleButton Selected="@(currentView == ViewMode.Grid)"
                                 SelectedChanged="@(_ => SetView(ViewMode.Grid))">
            DataGrid
        </ButtonGroupToggleButton>
    </TelerikButtonGroup>

    <span class="text-muted">@orders.Count orders loaded</span>
</div>

@code {
    private enum ViewMode { ListView, Grid }

    private List<SalesOrder> orders = new();
    private ViewMode currentView = ViewMode.ListView;

    protected override void OnInitialized()
    {
        orders = SalesService.GetOrders().ToList();
    }

    private void SetView(ViewMode view) => currentView = view;
}

In the code of the previous page, you can notice a few things:

  • We have created a enum with the options ListView and Grid, which will allow us to switch between views.
  • We load the orders into OnInitialized.
  • The method SetView allows changing the view.
  • We use a Blazor Button Group component, ideal for handling multiple grouped buttons. In this case, we implement it to activate one view or the other through the assignment of a state with Selected. Also, when a button is clicked, SelectedChanged is fired, which invokes the method SetView.

With the options section ready, let’s render the ListView component.

Rendering a Blazor ListView

The next step will be to render the ListView. To do this, we will add a conditional block and use the component TelerikListView:

@if (currentView == ViewMode.ListView)
{
    <TelerikListView Data="@orders"
                     Pageable="true"
                     PageSize="9">
        <Template Context="order">
            <div class="order-card">
                <div class="order-card-header">
                    <span class="order-card-id">#@order.OrderId</span>
                    <span class="status-badge status-@order.Status">@order.Status</span>
                </div>
                <div class="order-card-product">@order.Product</div>
                <div class="order-card-meta">@order.Customer   @order.Category</div>
                <div class="order-card-meta">@order.OrderDate.ToString("MMM dd, yyyy")</div>
                <div class="order-card-footer">
                    <span class="text-muted">@order.Quantity   @order.UnitPrice.ToString("C")</span>
                    <span class="order-total">@order.Total.ToString("C")</span>
                </div>
            </div>
        </Template>
    </TelerikListView>
}

In the previous code, the property Data binds to the service’s list. We configure some additional properties such as paging (Pageable) and number of items per page (PageSize). Also, we use a Template to define a custom view, highlighting only the most important data such as product, customer, category, date, total, etc.

To make the layout look correct, we will add some visual styles in wwwroot/app.css:

.sales-toolbar {
    display: flex;
    align-items: center;
    gap: 1rem;
    margin-bottom: 1rem;
    flex-wrap: wrap;
}

.order-card {
    border: 1px solid #e2e8f0;
    border-radius: 0.5rem;
    padding: 1rem;
    background: #fff;
    box-shadow: 0 1px 2px rgba(0,0,0,0.04);
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
    height: 100%;
}

.order-card-header,
.order-card-footer {
    display: flex;
    justify-content: space-between;
    align-items: center;
}

.order-card-product {
    font-size: 1.1rem;
    font-weight: 600;
    color: #0f172a;
}

.order-card-meta {
    color: #64748b;
    font-size: 0.9rem;
}

.order-total {
    font-weight: 700;
    color: #0f172a;
}

.status-badge {
    display: inline-block;
    padding: 0.15rem 0.6rem;
    border-radius: 999px;
    font-size: 0.75rem;
    font-weight: 600;
    text-transform: uppercase;
}

.status-Pending   { background: #fef3c7; color: #92400e; }
.status-Shipped   { background: #dbeafe; color: #1e40af; }
.status-Delivered { background: #dcfce7; color: #166534; }
.status-Cancelled { background: #fee2e2; color: #991b1b; }

When running the application, we can see the layout we created using the ListView:

Blazor ListView displaying sample sales orders

In the image above, you can notice that there is no comparison between orders; rather, there is navigation between the list items. If users wanted to group items, filter them or perform complex operations, this would not be the right component. Let’s now see how to implement a Grid.

Adding a Grid Component to the App

Let’s see how a Grid component looks, completing the else branch of the conditional in the code to add a TelerikGrid:

else
{
    <TelerikGrid Data="@orders"
                 Pageable="true"
                 PageSize="15"
                 Sortable="true"
                 FilterMode="@GridFilterMode.FilterRow"
                 Groupable="true"
                 Resizable="true"
                 Reorderable="true">
        <GridColumns>
            <GridColumn Field="@nameof(SalesOrder.OrderId)" Title="Order #" Width="110px" />
            <GridColumn Field="@nameof(SalesOrder.Customer)" Title="Customer" />
            <GridColumn Field="@nameof(SalesOrder.Product)" Title="Product" />
            <GridColumn Field="@nameof(SalesOrder.Category)" Title="Category" Width="140px" />
            <GridColumn Field="@nameof(SalesOrder.Quantity)" Title="Qty" Width="90px" />
            <GridColumn Field="@nameof(SalesOrder.UnitPrice)" Title="Unit Price" Width="130px" DisplayFormat="{0:C}" />
            <GridColumn Field="@nameof(SalesOrder.Total)" Title="Total" Width="130px" DisplayFormat="{0:C}" />
            <GridColumn Field="@nameof(SalesOrder.OrderDate)" Title="Date" Width="140px" DisplayFormat="{0:d}" />
            <GridColumn Field="@nameof(SalesOrder.Status)" Title="Status" Width="130px" />
        </GridColumns>
    </TelerikGrid>
}

In the previous code we can see the notable difference between the two components:

  1. It has parameters like Sortable, Groupable, Resizable, Reorderable, etc., which enable capabilities a user expects in a table-like format.
  2. The number of items shown per page is set to 15, because each row takes up less space.
  3. If needed, we could customize the columns to display custom content.

When running the application, we will have a result like the following:

Blazor DataGrid view displaying sales orders with columns

In the image above, you can see that we perform operations such as grouping, sorting and filtering rows.

Final Comparison Between Components

Once we have the application assembled and have seen how each component looks, we can reach the following conclusion:

  • Use a ListView when you want to treat each record as an individual entity, customizing its visual hierarchy. It is a component intended to be consumed by users.
  • Use a DataGrid when you want a spreadsheet-like experience, with operations such as sorting, grouping, filtering, etc. It is a component intended to be operated by users.

Each of the views has a different purpose, although it is possible to combine their use to create mixed experiences.

Conclusion

Throughout this article we have examined the ListView and Grid components from Progress Telerik UI for Blazor. We have discussed the best scenarios for using each of them, as well as the implementation code to use them.

We can conclude that you should use a ListView when the information is intended for end users, requires a high degree of customization and offers a unique exploratory experience.

On the other hand, a DataGrid can be used to display multiple records in a tabular form when operations that enable analysis are needed, such as grouping, sorting, filtering, etc.

Now I invite you to create spectacular experiences using both components. The whole Telerik UI for Blazor library is available in the free 30-day trial, including the ListView and the DataGrid.

Try Now

Read the whole story
alvinashcraft
just a second ago
reply
Pennsylvania, USA
Share this story
Delete

Finding the total number of processors on a machine with .NET

1 Share

This is a post about a simple concept. In .NET, I want to know how many processors exist on a host/VM. However, as far as I can tell, there's no APIs for that in modern .NET. If you need that information, this post shows the only approach I could come up with, which involves a P/Invoke on some platforms, and parsing files on Linux!

If there's a better way, please tell me, I kind of hate what I've had to do here 😅

Why not Environment.ProcessorCount?

Hopefully someone is thinking "Why wouldn't you just use Environment.ProcessorCount". After all, it's been available since .NET Framework 2.0! Unfortunately, what this value actually means depends on which version of .NET you're using…

  • .NET Framework—Returns the number of logical processors on the host machine (i.e. exactly what I want 🎉)
  • .NET Core < 6—Returns the number of logical processors on the host machine, but is container aware (to an extent, though it is buggy)
  • .NET Core 6+—Returns the number of logical processors on the host machine unless you're running with process affinity, or you're running in a container. Essentially it returns the number of processors available to the process.

Ironically, .NET Framework actually does exactly what I need and modern .NET doesn't 😅 That's generally understandable, as normally it's most useful to know how many processes a process has available, rather than how many the host has, but in this case, that's not what I want.

So what options do we have?

Could Microsoft.Extensions.Diagnostics.ResourceMonitoring be the answer?

Betteridge's law of headlines comes the for, the answer is "No" 😅

If you weren't aware, the Microsoft.Extensions.Diagnostics.ResourceMonitoring NuGet package provides a collection of APIs for monitoring the resource utilization (CPU, memory, network) of your .NET applications.

It provides two sets of APIs

You can use these metrics to emit a host of resource metrics:

  • container.cpu.limit.utilization
  • container.cpu.request.utilization
  • container.cpu.time
  • container.memory.limit.utilization
  • container.memory.usage
  • process.cpu.utilization
  • dotnet.process.memory.virtual.utilization
  • system.network.connections

However, you'll note that none of those metrics is the number of host processors. So we're out of luck.

Calling native APIs to get the details

I'll cut to the chase: the only way I found to retrieve the values I was after was to call native APIs:

  • On Windows, we need to P/Invoke GetActiveProcessorCount()
  • On macOS, we need to P/Invoke sysctlbyname("hw.logicalcpu")
  • On Linux, we need to read and parse /sys/devices/system/cpu/online

This is much messier than I had hoped, so if someone has a better approach, I'm all ears! Nevertheless, the following sections describe how to read each of these values on the three platforms.

Getting the total CPU count on Windows

I'll start with Windows, as it's one of the simplest. We simply make a call into the kernel, and invoke GetActiveProcessorCount passing in the "All processor groups" flag, so that we get the total number of processors on the system:

internal static class WindowsProcessorCount
{
    private const ushort AllProcessorGroups = 0xFFFF;

    internal static int? GetTotalProcessorCount(ILogger log)
    {
        var result = GetActiveProcessorCount(AllProcessorGroups);
        if (result > 0)
        {
            return result;
        }

        var error = Marshal.GetLastPInvokeError();
        log.LogWarning(
            "GetActiveProcessorCount failed when getting total machine processor count. ErrorCode={ErrorCode}",
            property: error);
        return null;
    }  

    [DllImport("kernel32.dll", SetLastError = true)]
    private static extern int GetActiveProcessorCount(ushort groupNumber);
}

This is very simple - the GetTotalProcessorCount method simply P/Invokes and returns the number of processors. If the returned value is 0 then we had an error, so we log it and return null.

Note that I've used [DllImport] in all these examples, as I needed to support .NET 6, but if you can, you should probably use [LibraryImport] instead.

That's Windows covered, on to the next OS!

Getting the total CPU count on macOS

The code on macOS is similarly a single P/Invoke, however it uses the generic sysctlbyname library function which requires a bit more faffing with arguments than Windows. Ultimately, it has essentially the same pattern as the Windows code.

internal static class MacOsProcessorCount
{
    private const string LogicalCpuName = "hw.logicalcpu";

    internal static int? GetTotalProcessorCount(ILogger log)
    {
        var size = new IntPtr(sizeof(int));
        var result = SysCtlByName(LogicalCpuName, out var value, ref size, IntPtr.Zero, IntPtr.Zero);

        if (result == 0 && value > 0)
        {
            return value;
        }

        var error = Marshal.GetLastPInvokeError();
        log.LogWarning(
            "sysctlbyname failed when getting total machine processor count. ErrorCode={ErrorCode}",
            property: error);
        return null;
    }

    [DllImport("libSystem.dylib", EntryPoint = "sysctlbyname", CharSet = CharSet.Ansi, SetLastError = true)]
    private static extern int SysCtlByName(
        string name,
        out int oldp,
        ref IntPtr oldlenp,
        IntPtr newp,
        IntPtr newlen);
}

So we essentially have the same pattern here: make the P/Invoke, check the result and return the value. Nothing too bad (though you obviously have to make sure to get the P/Invoke API correct, thankfully something that LLMs are very good at these days).

The final platform we have is Linux, which is where things get a bit different.

Getting the total CPU count on Linux

On Linux, rather than making a P/Invoke into a library, we instead read from the /sys/devices/system/cpu/online file, parse the list of CPUs and return the result.

Note that you could make a P/Invoke into the C library and call sysconf(_SC_NPROCESSORS_ONLN), but there were edge cases with calling sysconf I wanted to avoid, such as the fact the constant is difference on glibc vs musl etc. By reading the file directly, we avoid those issues.

internal static class LinuxProcessorCount
{
    private const string OnlineCpusPath = "/sys/devices/system/cpu/online";

    internal static int? GetTotalProcessorCount(ILogger log)
    {
        try
        {
            var contents = File.ReadAllText(OnlineCpusPath);
            var result = TryParseOnlineCpuRanges(contents.AsSpan());

            if (result is null)
            {
                Log.LogWarning(ex, "Parsing cpu-list failed: contents was not a valid cpu-list '{FileContents}'", contents);
            }
        }
        catch (Exception ex)
        {
            Log.LogWarning(ex, $"Error reading '${OnlineCpusPath}' to determine total machine processor count");
            return null;
        }
    }

    // Parses the Linux cpu-list-format (see https://www.kernel.org/doc/Documentation/admin-guide/kernel-parameters.txt)
    // This is a comma-separated list of either a single CPU index ("0") or an inclusive range ("0-7"), e.g. "0-3,4,8-11".
    internal static int? TryParseOnlineCpuRanges(ReadOnlySpan<char> contents)
    {
        var trimmed = contents.Trim();
        if (trimmed.IsEmpty)
        {
            return null;
        }

        var count = 0;
        var remaining = trimmed;
        while (!remaining.IsEmpty)
        {
            // Find the next token
            var commaIndex = remaining.IndexOf(',');
            var token = commaIndex < 0 ? remaining : remaining[..commaIndex];

            if (!TryParseToken(token, out var tokenCount))
            {
                // Should never happen, means the file contained invalid data
                return null;
            }

            // Increase the CPU count
            count += tokenCount;

            if (commaIndex < 0)
            {
                // All done
                break;
            }

            // Cut off the values we just read
            remaining = remaining[(commaIndex + 1)..];
            if (remaining.IsEmpty)
            {
                // trailing comma with no following token
                return null;
            }
        }

        // If we didn't read any values, something weird happened
        return count > 0 ? count : null;

        // Parse either a single value like "4", or a range, like "3-7"
        static bool TryParseToken(ReadOnlySpan<char> token, out int tokenCount)
        {
            tokenCount = 0;

            var dashIndex = token.IndexOf('-');
            if (dashIndex < 0)
            {
                // A single value
                if (!int.TryParse(token, out var single) || single < 0)
                {
                    return false;
                }

                tokenCount = 1;
                return true;
            }

            // Parse each value in the range
            var startSpan = token[..dashIndex];
            var endSpan = token[(dashIndex + 1)..];

            if (!int.TryParse(startSpan, out var start) || start < 0 ||
                !int.TryParse(endSpan, out var end) || end < start)
            {
                return false;
            }

            // Count the number covered by the range, e.g 0-3 = 4 CPUs
            tokenCount = end - start + 1;
            return true;
        }
    }
}

As I said earlier, this is a little annoyingly convoluted, but it's not complicated, it's just reading a file and parsing the contents 🙂

Putting it all together

So we now have a method for reading the total CPUs on each platform we can put it all together into one convenience method, that calls the correct API based on the platform:

internal static class TotalProcessorCount
{
    internal static int? GetTotalProcessorCount(ILogger log)
    {
        if (OperatingSystem.IsWindows())
        {
            return WindowsProcessorCount.GetTotalProcessorCount(log);
        }

        if (OperatingSystem.IsLinux())
        {
            return LinuxProcessorCount.GetTotalProcessorCount(log);
        }

        if (OperatingSystem.IsMacOS())
        {
            return MacOsProcessorCount.GetTotalProcessorCount(log);
        }

        return null;
    }
}

For this post I created the helper as a simple static type, but you would likely want to cache the value returned from GetTotalProcessorCount() seeing as it won't change for the lifetime of the process (unless we've got something wrong!). I'll leave that as an exercise for the reader, but for completeness, this is the full type, with the helper types nested inside to encapsulate them away

internal static class TotalProcessorCount
{
    internal static int? GetTotalProcessorCount(ILogger log)
    {
        if (OperatingSystem.IsWindows())
        {
            return WindowsProcessorCount.GetTotalProcessorCount(log);
        }

        if (OperatingSystem.IsLinux())
        {
            return LinuxProcessorCount.GetTotalProcessorCount(log);
        }

        if (OperatingSystem.IsMacOS())
        {
            return MacOsProcessorCount.GetTotalProcessorCount(log);
        }

        return null;
    }

    private static class WindowsProcessorCount
    {
        private const ushort AllProcessorGroups = 0xFFFF;

        internal static int? GetTotalProcessorCount(ILogger log)
        {
            var result = GetActiveProcessorCount(AllProcessorGroups);
            if (result > 0)
            {
                return result;
            }

            var error = Marshal.GetLastPInvokeError();
            log.LogWarning(
                "GetActiveProcessorCount failed when getting total machine processor count. ErrorCode={ErrorCode}",
                property: error);
            return null;
        }  

        [DllImport("kernel32.dll", SetLastError = true)]
        private static extern int GetActiveProcessorCount(ushort groupNumber);
    }

    private static class MacOsProcessorCount
    {
        private const string LogicalCpuName = "hw.logicalcpu";

        internal static int? GetTotalProcessorCount(ILogger log)
        {
            var size = new IntPtr(sizeof(int));
            var result = SysCtlByName(LogicalCpuName, out var value, ref size, IntPtr.Zero, IntPtr.Zero);

            if (result == 0 && value > 0)
            {
                return value;
            }

            var error = Marshal.GetLastPInvokeError();
            log.LogWarning(
                "sysctlbyname failed when getting total machine processor count. ErrorCode={ErrorCode}",
                property: error);
            return null;
        }

        [DllImport("libSystem.dylib", EntryPoint = "sysctlbyname", CharSet = CharSet.Ansi, SetLastError = true)]
        private static extern int SysCtlByName(
            string name,
            out int oldp,
            ref IntPtr oldlenp,
            IntPtr newp,
            IntPtr newlen);
    }

    internal static class LinuxProcessorCount
    {
        private const string OnlineCpusPath = "/sys/devices/system/cpu/online";

        internal static int? GetTotalProcessorCount(ILogger log)
        {
            try
            {
                var contents = File.ReadAllText(OnlineCpusPath);
                var result = TryParseOnlineCpuRanges(contents.AsSpan());

                if (result is null)
                {
                    Log.LogWarning(ex, "Parsing cpu-list failed: contents was not a valid cpu-list '{FileContents}'", contents);
                }
            }
            catch (Exception ex)
            {
                Log.LogWarning(ex, $"Error reading '${OnlineCpusPath}' to determine total machine processor count");
                return null;
            }
        }

        // Parses the Linux cpu-list-format (see https://www.kernel.org/doc/Documentation/admin-guide/kernel-parameters.txt)
        // This is a comma-separated list of either a single CPU index ("0") or an inclusive range ("0-7"), e.g. "0-3,4,8-11".
        private static int? TryParseOnlineCpuRanges(ReadOnlySpan<char> contents)
        {
            var trimmed = contents.Trim();
            if (trimmed.IsEmpty)
            {
                return null;
            }

            var count = 0;
            var remaining = trimmed;
            while (!remaining.IsEmpty)
            {
                // Find the next token
                var commaIndex = remaining.IndexOf(',');
                var token = commaIndex < 0 ? remaining : remaining[..commaIndex];

                if (!TryParseToken(token, out var tokenCount))
                {
                    // Should never happen, means the file contained invalid data
                    return null;
                }

                // Increase the CPU count
                count += tokenCount;

                if (commaIndex < 0)
                {
                    // All done
                    break;
                }

                // Cut off the values we just read
                remaining = remaining[(commaIndex + 1)..];
                if (remaining.IsEmpty)
                {
                    // trailing comma with no following token
                    return null;
                }
            }

            // If we didn't read any values, something weird happened
            return count > 0 ? count : null;

            // Parse either a single value like "4", or a range, like "3-7"
            static bool TryParseToken(ReadOnlySpan<char> token, out int tokenCount)
            {
                tokenCount = 0;

                var dashIndex = token.IndexOf('-');
                if (dashIndex < 0)
                {
                    // A single value
                    if (!int.TryParse(token, out var single) || single < 0)
                    {
                        return false;
                    }

                    tokenCount = 1;
                    return true;
                }

                // Parse each value in the range
                var startSpan = token[..dashIndex];
                var endSpan = token[(dashIndex + 1)..];

                if (!int.TryParse(startSpan, out var start) || start < 0 ||
                    !int.TryParse(endSpan, out var end) || end < start)
                {
                    return false;
                }

                // Count the number covered by the range, e.g 0-3 = 4 CPUs
                tokenCount = end - start + 1;
                return true;
            }
        }
    }
}

Should I use this code?

That's entirely up to you 😅 I haven't yet shipped this code into production, but I'm seriously considering it. I think it's pretty sound as best as I (and the 🤖) can tell, but obviously use your own judgement. As I said before, if you know of a better way to get these values, I'd be very interested to hear about it in the comments.

The one thing I would suggest changing if you're using modern .NET applications with dependency injection etc, is to nest all this code inside a little singleton wrapper that caches the value for the lifetime of the process and provides an ILogger instance to use etc. But otherwise, try it out, make sure it works for you!

Summary

In this post I talked about how to find the total number of CPUs available on a host, as opposed to the number of CPUs available to a process. Environment.ProcessorCount returns the former in .NET Framework, but in .NET Core, it returns the latter (and you can actually trust the values from about .NET 6+). However, in .NET 6+, if you actually want the total number of CPUs on the host, then there are no managed APIs I could find in the BCL to achieve that.

As a consequence, in this post, I show how to find the total processor count on Windows, macOS, and Linux. For Window and macOS, we can use a simple P/Invoke to read the value. This is theoretically available on Linux, but it's a bit harder than you might expect, so instead of using P/Invoke, I show how to read and parse the /sys/devices/system/cpu/online file instead. Finally, I put all three approaches into a helper that switches based on the current platform.

Read the whole story
alvinashcraft
29 seconds ago
reply
Pennsylvania, USA
Share this story
Delete

Cursor Releases Origin as an Agent-Native Alternative to GitHub

1 Share

AI coding agent Cursor has launched Origin, a git based code hosting platform embedded inside its AI-powered editor, positioning it as an alternative to GitHub for teams that already work in Cursor. Origin is rolling out in early beta on Pro, Teams and Enterprise plans, and lives inside a new Codebase tab within the Cursor application.

By Matt Saunders
Read the whole story
alvinashcraft
43 seconds ago
reply
Pennsylvania, USA
Share this story
Delete

Using Historical Data to Confirm Performance Improvements

1 Share

Using historical data to confirm performance improvements turns a fast test into evidence the business can defend.

Using historical data to confirm performance improvements starts with one fair question. Did the same work become cheaper under comparable conditions? In an illustrative case, a DBA deploys an index at 9:00 AM and watches average reads fall. The release channel fills with check marks.

Before deployment, the query averaged 42,000 logical reads across 18,400 executions. Afterward, it averaged 6,100 reads across 11,200 executions. Total reads fell 91 percent, but executions fell 39 percent too.

That missing context changes the decision. The team now needs matched windows, stable plans, and comparable parameters. Enough executions must pass before anyone calls the index a success.

One Fast Execution Proves Almost Nothing

A clean test confirms a change for one parameter set. It cannot represent every customer, data distribution, or concurrency pattern. Cached pages and quiet servers can flatter results while hiding blocking, memory pressure, or storage latency.

Start with per-execution duration, CPU, logical reads, writes, and waits. Then add execution count. If executions double after latency halves, aggregate duration stays level. Total CPU and reads still require separate calculations.

Both views matter because users experience individual calls, while servers absorb the complete workload. Application growth may raise daily CPU despite better per-call latency. Modest per-call gains may also save substantial capacity on frequent statements. Report both perspectives instead of selecting the flattering number.

Using Historical Data to Confirm Performance Improvements historical-performance-proof-scaled

Use a Before-and-After Scorecard

Measure What to Compare
Duration Typical behavior and slow outliers
CPU and reads Per execution and total workload
Executions Count, application, and parameter mix
Plan Plan identity and estimate quality
Writes DML latency, log volume, and maintenance

Build Comparable Workload Windows

Compare Monday morning with another normal Monday morning, not Sunday night. Match business cycles, batch schedules, release activity, and expected traffic. Keep important database, hardware, and configuration conditions consistent.

The plan may change when a large customer replaces a small one. Match applications, databases, users, query signatures, and parameter patterns. Note any statistics update or plan change inside either window.

Use several windows when the workload varies naturally. One favorable hour may be noise, while repeated improvements establish a pattern. Include enough executions to limit isolated outliers, and preserve the exact deployment time.

Normalize totals when windows cover different durations, but never hide the raw values. Rates per minute help compare uneven windows, while counts preserve capacity impact. Separate scheduled jobs from interactive traffic when their patterns differ. Otherwise, one overnight process can make a healthy daytime change look unsuccessful.

Read Product History With Context

SQL DM from IDERA charts query history for average duration, CPU, reads, writes, waits, blocking, deadlocks, and CPU per second. Event occurrences add execution-level statistics and SQL text. Now the graph answers the useful question: did the query stay faster during real traffic?

That history still reflects collection settings. Filters, thresholds, disabled monitoring, and retention choices can create gaps. Older query records may be aggregated into daily summaries, which suppresses some statement, client, and user detail. Repository grooming can also remove data beyond the configured retention period.

Use Query Store as a Second Witness

Query Store persists query text, plans, and runtime statistics. SQL Server 2017 and later can capture query-level waits. This historical evidence can connect an improvement with an index, plan, or workload change.

Query Store is not a recording of every execution. Runtime statistics are aggregated into configurable time intervals. Its averages, minimums, maximums, and standard deviations describe each plan within those intervals. Capture policies, cleanup settings, and storage limits determine what remains available.

Compare plan identifiers as well as query identifiers, because lower duration may come from an unrelated new plan. A forced plan or statistics refresh may alter the result. The claim gets stronger when the plan, change, and result share one clear timeline.

Measure the Cost of the Improvement

An index can reduce reads for selected queries while increasing work for data changes. Check insert, update, and delete activity on the affected table. Review index size, maintenance time, logging, lock behavior, and storage consumption. Confirm that neighboring queries did not regress.

Native index usage counters can reveal seeks, scans, lookups, and update maintenance. However, those counters reset after events such as a server restart. Record the observation start time, because a short window may miss monthly reports depending on the index.

Define success and guardrails before deployment, such as lower reads without raising write latency beyond an agreed threshold. Capture the same metrics after deployment for an equivalent business window. Keep a rollback script available until the evidence remains stable.

The Fair Counterargument

Controlled benchmarks can demonstrate causality better than messy production history. A test regression costs nothing, while a production regression costs customers. That argument holds when test data and execution conditions represent production. Laboratory testing makes repeated measurements safer, especially when schema changes carry real risk.

However, controlled tests remove the concurrency, parameter diversity, and operational surprises that often determine production performance. Historical monitoring supplies that missing context. The strongest conclusion combines controlled testing with comparable production windows. Neither source should carry the decision alone.

A Result Worth Keeping

Baselines provide a comparison. SQL DM from IDERA supports a moving seven-day dynamic baseline and fixed custom periods. Choose normal periods and exclude quiet hours that distort expected behavior. A baseline is a reference, not an automatic verdict.

A trustworthy report shows the gain and every reason it might be misleading. Name the change, workload window, execution count, plans, and resource effect. Document competing deployments, missing data, and the period of stable behavior.

The DBA from 9:00 AM should wait through the next comparable peak. If reads stay lower and the guardrails hold, the change has earned its place. Then write the result down, because next quarter nobody will remember the details.

A fast test opens the case. A faster workload earns the decision.

Reference: Pinal Dave (https://blog.sqlauthority.com/), X

First appeared on Using Historical Data to Confirm Performance Improvements

Read the whole story
alvinashcraft
1 minute ago
reply
Pennsylvania, USA
Share this story
Delete

T-SQL Tuesday #201 Round-Up: Temp Tables, Friend or Foe?

1 Share

The T-SQL Tuesday #201 round-up: nine bloggers on whether temp tables are a friend or a foe, from full-throated defenses to a lab built to prove me wrong.

The post T-SQL Tuesday #201 Round-Up: Temp Tables, Friend or Foe? appeared first on SQLServerCentral.

Read the whole story
alvinashcraft
1 minute ago
reply
Pennsylvania, USA
Share this story
Delete
Next Page of Stories