Skip to content
This repository has been archived by the owner on Dec 24, 2023. It is now read-only.

Logging vs. Exceptions vs. Result Objects #2

Open
dbwiddis opened this issue Jan 2, 2019 · 39 comments
Open

Logging vs. Exceptions vs. Result Objects #2

dbwiddis opened this issue Jan 2, 2019 · 39 comments

Comments

@dbwiddis
Copy link
Member

dbwiddis commented Jan 2, 2019

Early in OSHI's development, the code was littered with UnsupportedOperationExceptions. This seems an appropriate response when we're asking for data that simply doesn't exist (e.g., load average on Windows). But it requires the user to explicitly handle these exceptions.

I moved away from exceptions into log messages, which allowed a more finely grained control of what was a normal problem vs. failure (warnings vs. errors etc.) and just returned sensible defaults (negative values, or 0, or empty strings, or empty collections). This introduces the opposite problem, of not allowing the user to handle exception types.

OSHI needs a standardized method of handling these types of situations:

  • Data is undefined on a platform (Windows load avg.)
  • Data is not easily obtained on a platform (Windows open files)
  • Data requires elevated permissions and/or software installs that the user doesn't have (lots of Linux stuff)
  • Data should normally return but happened to fail in this case (Sensor readings) but the user can try again
  • Data wasn't updated because you asked for it too recently (tick counts < 1 second apart).

We can use Optional results in key places, or encapsulate the result in our own OshiResult class that includes:

  • Result object
  • Result type (similar to how VARIANT is used in JNA)
  • Result timestamp
  • Result error value/enum/etc.
@dblock
Copy link

dblock commented Jan 2, 2019

I like this problem. Curious why you didn't specialize exceptions @dbwiddis?

It feels to me that if I am, for example, in need of elevated permissions to get X, I should get an AccessDenied exception. That said, X may be part of a parent object Y, and I should only get the exception when I do Y.X, and not when I get Y.

@cilki
Copy link
Collaborator

cilki commented Jan 3, 2019

The concept of API extensions for different platforms like @YoshiEnVerde suggested solves the UnsupportedOperationException problem because it would be impossible to call a method that is unsupported in the first place. For example you would receive a DiskWindows object that can't do things that a DiskLinux can do.

When there's a permissions error or missing software, then the OshiResult can contain the error or a relevant exception can be thrown. I'm not sure which I think is better yet.

@dbwiddis
Copy link
Member Author

dbwiddis commented Jan 3, 2019

Curious why you didn't specialize exceptions @dbwiddis?

As you may recall when you recruited me, I'm an amateur/hobbyist programmer without formal training and with a limited skill set. I didn't even know I could specialize exceptions when I got started. :)

This project has been a great learning experience for me and now that I know better.

For example you would receive a DiskWindows object that can't do things that a DiskLinux can do.

But that removes the platform-independent API that is the core of the project. I want my CentralProcessor interface to have a getLoadAverage() method without knowing or caring which operating system is returning the result. And thus, CentralProcessorWindows must implement this method and do something with it, either returning a default value or empty or null array or throwing an exception, or returning an Optional with no value present.

@cilki
Copy link
Collaborator

cilki commented Jan 3, 2019

But that removes the platform-independent API

Both CpuWindows and CpuLinux would extend Cpu which contains features defined on all platforms. If the user knows what OS they are on and wants platform-specific features, they can ask for a CpuLinux. Otherwise they will get a Cpu by default which is platform-independent. (Under the hood, the Cpu instance will really be a CpuLinux)

Since Windows is missing load average, it unfortunately cannot be included in the base type Cpu without reintroducing the problem we are trying to solve. This could be mitigated by creating groups of similar platforms like CpuUnix. That way the user at most needs to know they are on some kind of *nix in order to ask for the load average.

@cilki
Copy link
Collaborator

cilki commented Jan 3, 2019

This is basically how it would look. If we strictly follow the "Cpu contains only attributes defined on all platforms" principle, then there are no special cases.

api

@YoshiEnVerde
Copy link
Collaborator

With one of the objectives for this version being decoupling the fetching from the API, we need to think of both parts with different design eyes.

With OSHI trying to both have a small footprint and being performant (which, most of the time, are mutually exclusive things), we can use this to tweak details for the best.

For example: Using immutable objects is great for concurrency and security, but a nightmare on the memory resources, even with caching mitigating things a bit.

The opposite is also true: A very common solution for Java systems that need to keep resource consumption to a minimum is to offload the costs from the memory to the CPU. This is done by saving all the data in the cheapest representation possible, then casting/parsing the data every single time a read/write op is requested.
For example, when we fetch some info from a Linux command (e.g.: dmidecode), instead of asking for a specific field, we can ask for every single field that request can give us in one go, then convert the whole string result into a byte[]. Whenever a request is made for one of the values returned by that call, we parse the contents of the byte[] accordingly.

This second approach is of note for all our info sources that involve parsing OS command results. Specially so, since we could also keep a cache in the next layer for the parsed values themselves, and the data updates will be manually triggered.

@dblock
Copy link

dblock commented Jan 3, 2019

+1 for using basic inheritance (@cilki's example with CPU) to avoid having fields that just don't exist on a specific OS

@dbwiddis
Copy link
Member Author

dbwiddis commented Jan 3, 2019

I'm not so fond of using inheritance to solve this problem, since Java doesn't support multiple inheritance and it's a more finely grained issue than OS, and exceptions are common.

Windows doesn't have load average, so in the example above, we create a *nix parent. But macOS doesn't have Disk Queue Length (so do we create a parent of all OS's except Mac for the Disks?)

Mac doesn't have IOWait and IRQ information, and Windows doesn't have Nice and IOWait, so should we have different array sizes here? (or is returning "0" okay as we currently do?) Some stuff is just insanely slow on Windows (counting file handles) so we omit it, while letting the user know it's just "moderately" slow on *nix systems.

Some features are version-dependent (particularly newer vs. older Windows versions or 32- vs. 64-bit) or language dependent, or dependent (especially on *nix) on whether the user has root priviliges (e.g., dmidecode) or has installed a software package (e.g., lshal).

There's stuff that we'd like to put in (Windows Services) that has more information (running/stopped) than the equivalent in other OS's (which are just a list of text strings and don't correlate to process IDs).

Then there's the Windows Sensor stuff, that pretty much relies on OpenHardwareMonitor running in the background, which can in theory be turned on and off while OSHI is running. If it's on, we can return values for some sensors, but what do we return if we only have temperature but not fan speed? If it's off (or OHM doesn't detect fans) do we re-generate a new object with different inheritance that doesn't have a getFanSpeeds() method? When do we switch back? Do we insist on "no third party" programs and just tell users we don't do Windows sensors? How about information we can get from *nix systems if X is running, but can't if it's not?

I could go on, but the point is, we can't solve all of these problems with inheritance and if we do OS-specific code we violate DRY a lot. We have to have a way have a common command across most OS's that we can have a standardized "not on this OS" response.

@dbwiddis
Copy link
Member Author

dbwiddis commented Jan 3, 2019

The gist of my above comment is that I'd prefer:

  • The existing common interface, and individual OS classes. If a single OS doesn't have something common to all the other OS's (e.g., Disk queue length or load average) throw an UnsupportedOperationException (or similar).
    • I'm leaning in OSHI 4 to a serializable POJO "Data Object" as a top-level inheritance for the class variables we plan to return, from which platform specific classes inherit (sometimes with an additional abstract class layer). As well as a class-independent "Util" class that does calculations (e.g., for the CentralProcessor the data object would maintain current ticks+timestamp and previous ticks+timestamp, while the util class would have the getCpuUsageBetweenTicks() method.
  • Individual functions/methods on single OS's (e.g., Windows services)
    • We will need a centralized way to document these for users to know they are available, without surfing every page in the API.
  • Multiple ways to fetch data implemented as "drivers" (e.g., PDH, WMI, command line) that can either be manually or automatically configured as the source for information.
  • A common exception type that tells the user there's "no data" or "no new data" (NotFoundException? AccessDeniedException?)

@dblock
Copy link

dblock commented Jan 3, 2019

I love the detailed examples here @dbwiddis. ... scratching my head mostly at this point :)

@YoshiEnVerde
Copy link
Collaborator

YoshiEnVerde commented Jan 3, 2019

Sorry about the following wall of posts, but I didn't want a single monolothic post that became unreadable

The main problem with not using inheritance is that it defeats pretty much 90% of the point of using OSHI, outside of not having to bother implementing your own calls/parsers for the system values.

Any use case that depends on getting a specific value that can only be recovered in a specific O.S. for a specific architecture, under a specific hardware stack, voids every single point of having a common library.

If you need an ID that can only be gotten in Windows, for a specific windows-only type of service, that can only be gotten in 32bit archs, for a SPARC machine, then you'd be best served just doing a straight call for the service, instead of importing OSHI.
Just copy the corresponding method from OSHI and you'll be done with it.

The idea behind having inheritance is that the 85% of the data that is, relatively, common between all systems should be accessible in three or four lines, without ever even caring what tech stack is under you.

That's also the point of a standardized API, to remove the complexity of all the details, and to abstract the implementation, for the user.

If you need to manually check which OS you're working under, because you have special use-cases for each stack, then you're already doing that check by hand, you can just as well do a manual cast for the returning object, or call a more specialized API (that we would also be providing).

@YoshiEnVerde
Copy link
Collaborator

As counter examples:

  • I need to check every hour, or so, how much free space I have in my disks. I shouldn't have to care how each OS handles that, I should just call a single method and get a single value back: double System.Memory.Disks.FreeSpace()
  • I need to get some serial numbers from the system, I shouldn't care what the exact system is, just call a single method for the values

@YoshiEnVerde
Copy link
Collaborator

The point is that we should have multiple layers of interaction:

  • Public Facing

    1. Standardized API: Only values that are shared between stacks. Both calls and return types should be standardized for every value, to make it easier to handle interoperability
    2. Stack Specific API: Here we can have all the details and values that are O.S. specific, or Arch specific. These can be called directly, if we have a set stack, or we can use some promotion mechanism to reach them from [1]
    3. Utilities API: These should contain the elements needed to operate on the values from [2], to move from [1] to [2] (or vice-versa), for common utils for [1], etc.
  • Internals

    1. Drivers: These are the internals that actually gather the values. Methods and value types for this layer should not reflect on methods and values from [1] or [2]
    2. Cache: This handles caches, staleness, refreshes, etc

@YoshiEnVerde
Copy link
Collaborator

Matter of fact is that, currently (OSHI3) there is NO real common interface.

Because we have elements in there that are OS specific (or stack specific), they fail if the OS/Stack is not the expected one, and they basically require the user to already know what they were going to find before calling OSHI, in the end the common interface is just a fallacy.

Even worse, because of that common interface, the values returned are inconsistent across platforms. For example, the issue that originally brought me to OSHI: For one OS GetSerialNumber() returns the OS serial number, for another OS it returns the MOBO serial number, for another it returns the CPU's.

@YoshiEnVerde
Copy link
Collaborator

What we should do is define what values we consider to be standard, and which type they should be.
If a specific stack cannot recover that field, we can return some unavailable value.

For example:

  • Let's say that 9 out of 10 OS available for OSHI can be queried for a Memory Stick Serial Number (just a made up example).
  • We've also defined that all S/N values should be string for back/forth compatibility reasons.
  • Besides that, we've reached the consensus that we'll use Result objects, containing the value or the error accordingly.

So, for 9 OSs, when the method is called, the driver/cache is queried, a value is recovered, then cast into a string, set into a Success Return Object, and returned.
For the 10th OS, the corresponding error will be set into a Failure Return Object, and returned.

@YoshiEnVerde
Copy link
Collaborator

So, why allow this for values that are in most stacks, but not for values that are in specific stacks?
Mainly, because we should be striving for a standardized API that can be used by any user without having to know/check if their usage is valid for every single one of their use cases.

If we did the same for a value that is only available in 64bit Debian Linux (for example), then every user of the API will need to:

A. Check that every single method they try to use will conform to their intended use cases
B. Make sure that every one of their users will never use their code in an unintended stack
C. Program in specialized handling code for every single possible stack they might encounter

If we standardize, then offer the capability of promoting to specialized APIs, everybody can follow the logic of:
A. Program for any use case, by using the Standardized API
B. If you have a very specific use case you need to cover, promote to a Specialized API

@dbwiddis
Copy link
Member Author

dbwiddis commented Jan 3, 2019

The main problem with not using inheritance is that it defeats pretty much 90% of the point of using OSHI ... Any use case that depends on getting a specific value that can only be recovered in a specific O.S. for a specific architecture, under a specific hardware stack, voids every single point of having a common library.

Did you mean to say not there?

This isn't the case we're discussing. We're discussing a value that is common across most OS's but missing on one or a few. (e.g., Load average).

By using the above inheritance pattern (a CpuUnix object) I would not be able to ask the Cpu class for the load average, as it wouldn't be part of that parent class. I would have to know that I was on a *nix-based system and that the load average method was available to me via class casting to CpuUnix.

Your counter examples are specifically addressed by the current common interface-based API implementation. The user should not care what OS they are on when they ask for a Load Average. The user should be notified (via exception or sensible default or documented invalid value such as a negative number) if what they ask for isn't provided by the system they are monitoring.

What we should do is define what values we consider to be standard, and which type they should be.
If a specific stack cannot recover that field, we can return some unavailable value.

This is exactly what I'm proposing, except with more options/detail than just "unavailable". There's an "unavailable now but try again" vs. "unavailable ever on this system" vs. "unavailable but if you run with elevated permissions it might be" vs. "unavailable but if you install this package or run this third party software I can use it".

@YoshiEnVerde
Copy link
Collaborator

YoshiEnVerde commented Jan 3, 2019

This isn't the case we're discussing. We're discussing a value that is common across most OS's but missing on one or a few. (e.g., Load average).

By using the above inheritance pattern (a CpuUnix object) I would not be able to ask the Cpu class for the load average, as it wouldn't be part of that parent class. I would have to know that I was on a *nix-based system and that the load average method was available to me via class casting to CpuUnix.

No it shouldn't. If you're talking about a value that is there for almost every OS, but a few, and we consider it to be standard enough to add it to the Standardized API, then for that OS it would return a failure by not supported.

If OSHI fails because of issues unrelated to that, it should return the corresponding error: i.e.: failure by not available, failure by forbidden, etc

@YoshiEnVerde
Copy link
Collaborator

Single inheritance in Java is not an error, it's part of the design. We've been working with that for over 20 years.

That's why Interfaces were added.
And why, before that, we used more composition.

  • What you do is define multiple (almost) functional interfaces, then have the corresponding classes implement them.
  • For shared code, you use classical inheritance (class to class).
  • For the equivalent of multiple class inheritance, you hybridize composition and facade:
    • Multiple functional classes that would compare to the multiple parent classes you want to inherit at the same time
    • A child class that contains instances of the pseudo-parent classes, and implements a facade pattern for their methods

@dbwiddis
Copy link
Member Author

dbwiddis commented Jan 3, 2019

If you're talking about a value that is there for almost every OS, but a few, and we consider it to be standard enough to add it to the Standardized API, then for that OS it would return a failure by not supported.

I agree -- I would much rather provide load average, and do something different for Windows only. I would much rather provide Disk Queue length and do something different for macOS only. This is the philosophy I've went with on 3.x, returning documented values when not available (e.g., 0).

I have a feeling we're saying the same thing here, but getting wrapped up in how "inheritance" applies to this paradigm. I don't see a need for inheritance in the interface-exposed classes. There's some value in inheritance to remove redundancy in the access/drivers (we have common JNA-based unix functions, for example).

@YoshiEnVerde
Copy link
Collaborator

I have a feeling we're saying the same thing here, but getting wrapped up in how "inheritance" applies to this paradigm. I don't see a need for inheritance in the interface-exposed classes. There's some value in inheritance to remove redundancy in the access/drivers (we have common JNA-based unix functions, for example).

LOL, yes, I get the same feeling

@YoshiEnVerde
Copy link
Collaborator

As a last quick caveat:
My idea is to have two tiers of APIs, instead of just one:

  1. The Standardized API, which is a refinement of the current common interface
  2. The Specialized API, which would be the interface for the OS specific methods

Here's the issue we're having:
Interface in implementation means one thing (the almost-class thingie), Interface in design means a very different thing (the set of public parts of the module that are available to the user, plus their documentation and contracts)

@dbwiddis
Copy link
Member Author

dbwiddis commented Jan 3, 2019

For your next trick, tell me how I should handle per-core CPU frequency. :) (We currently have a CPU interface but should consider having "physical processor" objects on that, which have frequency, and "logical processor" objects on those physical processors. :)

@YoshiEnVerde
Copy link
Collaborator

For your next trick, tell me how I should handle per-core CPU frequency. :) (We currently have a CPU interface but should consider having "physical processor" objects on that, which have frequency, and "logical processor" objects on those physical processors. :)

  1. Is this something we can calculate for every (or most) OSs we support?
  2. If so, is this something that will affect every underlying architecture?

If both are true, then we just need to emulate the same architecture in our objects:

> CPU 
    > Processors
        > Physical
           > Logical per Physical (refs to the same objects as in the next item)
           - Values corresponding to physical processors
        > Logical
           - Values corresponding to logical processors

If one of the two answers is a no, then we only implement the design for the valid one (if any) in the Standardized API, and we implement the others in each stack, when applicable

@YoshiEnVerde
Copy link
Collaborator

The main thing is that we can have multiple layers to promote to, not just OS specific.

For example, let's say we have a 50-50 split on the processors example (or even an 8:2 ratio, so long as at least 2 stacks share a functionality)

Then, we can have the following (impl) interfaces:

  • LogicalProcessorsAvailable
  • PhysicalProcessorsAvailable

The Standardized interface CPU might define a very basic method GetProcessorFrecuency(), that returns a single value for the whole CPU (probably the average of all the underlying processors).

Then, the other two would have a set of methods GetPhysicalProcessors() and GetLogicalProcessors() that return collections of the corresponding PhysiscalProcessor and LogicalProcessor interfaces.

Thus, if the user wants more granular details, they might do:

var cpu = Oshi.GetCpu(); //Returns an implementation of the CPU interface
if(cpu instanceof LogicalProcessorsAvailable)
{
    var logicalProcs = ((LogicalProcessorsAvailable) cpu).GetLogicalProcessors();
    for (LogicalProcessor proc : logicalProcs) 
    {
        //Do whatever you wanted to do with the logical processors
    }
}
else
{
    //Handle the possibility of not having access to logical processors
}

@cilki
Copy link
Collaborator

cilki commented Jan 3, 2019

The main thing is that we can have multiple layers to promote to, not just OS specific.

That would definitely achieve the granularity required, but also entails many more interfaces for the user to know about. Maybe that can be abstracted away from the usage, but I think it would get fairly complicated internally. Will need some time to process this.

@YoshiEnVerde
Copy link
Collaborator

Yes, it would.
That's why I only proposed the idea for the more important functionalities...

The way I'm imagining this is that we can have a tutorial/how to/getting started doc that explains the standardized API, without all the bells and whistles.
Basically, how to use OSHI, how to configure it, how to reach all the standard functionalities.

Then, we have a kind of Advanced Manual with all the more granular interfaces.

The idea would be that the standardized interfaces should cover most use cases for most users, leaving the few users that need a specific functionality with checking the advanced manual (or the full JavaDoc ref) for the specific interface they might need.

@dbwiddis
Copy link
Member Author

dbwiddis commented Jan 3, 2019

Is this something we can calculate for every (or most) OSs we support?

Linux, FreeBSD, and Solaris have per-logical-core values (although the reality is the source value is per-physical core). Windows and MacOS have one singular value for the whole CPU which would be a reasonable default to replicate across all processors.

we just need to emulate the same architecture in our objects:

Which was more the point of my question. In the real world, we have a logical processor ("soft"ware object) which is one of maybe multiple on a physical processor (a.k.a. core, a hardware object) which is one of maybe multiple on a package (a.k.a. socket, a hardware object) which is one of maybe multiple on the overall CPU. Software (e.g., /proc/cpu) returns the current OSHI per-processor output (cpu ticks) at the logical processor level but properly evaluating load requires comparison at the physical processor (core) level... e.g., the sum of ticks on processor 0 and 1 are what I care about; so I would like the getCore() method of the logicalProcessor object to return "core 0" simply by evaluating that method on its parent object. Similarly "core 0" shares its currentFrequency with the package it's on so getPackage() on the core should return "package 0". And all those can get the CPUID info from getProcessorId() on their parent.

Or is creating 8 logical processor objects, each with its own CPU ticks, overkill when I have easy access to a 2D array with that info at the top level?

@YoshiEnVerde
Copy link
Collaborator

If we can set up some simple rules/generalities on how to do this, we can follow the code first standardize later approach:

  1. We start with a very basic standardized set of interfaces and classes, like System, OS, CPU, Network, etc.
  2. Every time we add a functionality to enough specialized interfaces, we abstract it out into the standardized interfaces

What we need the most for this approach is a well defined procedure for adding functionalities. That way, we avoid the tangle that comes out of the same functionality returning an Integer in Windows, a String in Debian Linux, a set of 3 enum values in Fedora Linux, and a complex object in OSX.

Something like:

  • String and Boolean returns by default
  • Numeric returns if the type of value returned could never be anything but numeric (like frecuencies, latencies, sizes, etc)
  • Avoid enums before standardization
  • Complex results should always be exploded into sub-interfaces and/or extra values
    etc

@YoshiEnVerde
Copy link
Collaborator

Is this something we can calculate for every (or most) OSs we support?
Linux, FreeBSD, and Solaris have per-logical-core values (although the reality is the source value is per-physical core). Windows and MacOS have one singular value for the whole CPU which would be a reasonable default to replicate across all processors.

we just need to emulate the same architecture in our objects:
Which was more the point of my question. In the real world, we have a logical processor ("soft"ware object) which is one of maybe multiple on a physical processor (a.k.a. core, a hardware object) which is one of maybe multiple on a package (a.k.a. socket, a hardware object) which is one of maybe multiple on the overall CPU. Software (e.g., /proc/cpu) returns the current OSHI per-processor output (cpu ticks) at the logical processor level but properly evaluating load requires comparison at the physical processor (core) level... e.g., the sum of ticks on processor 0 and 1 are what I care about; so I would like the getCore() method of the logicalProcessor object to return "core 0" simply by evaluating that method on its parent object. Similarly "core 0" shares its currentFrequency with the package it's on so getPackage() on the core should return "package 0". And all those can get the CPUID info from getProcessorId() on their parent.

Or is creating 8 logical processor objects, each with its own CPU ticks, overkill when I have easy access to a 2D array with that info at the top level?

The problem is that double[][] instead of List<Processor> is the kind of thing you have to wonder at low-level, not at API level.

You're only thinking of the frecuencies there, and then it would be more performant that way.

However, if you actually have a dozen values per processor (S/N, Frecuency, Thermals, Type, Enabled, State, etc), you're now talking about a dozen type[][] that you have to manually handle each time, instead of just a collection of processors.

More so, we come back to the original question: Is this something that is standard for all stacks? Or specific to a select few?
Because, if it's standard for most/all stacks, we can just as well have a single field/value in the processor for each 2d matrix and be done with it, because it would be something that all implementations of the standard interface should be able to return (even if a couple might give the failure by not available error)

@dbwiddis
Copy link
Member Author

dbwiddis commented Jan 3, 2019

* String and Boolean returns by default

An easy case. Or not really. What if the boolean result is "unknown"?

* Numeric returns if the type of value returned could **never** be anything but numeric (like frecuencies, latencies, sizes, etc)

Do you literally mean the Java 'Number' object? Because we have access to UINT64 data that should be properly returned as a BigDecimal but right now we just strip the sign bit and return a long.

The problem is that double[][] instead of List is the kind of thing you have to wonder at low-level, not at API level.
You're only thinking of the frecuencies there, and then it would be more performant that way.

Good point. We probably don't need the whole inheritance structure and can just do a List<LogicalProcessor> then, with each LogicalProcessor object containing:

  • processor # (should match its order in the List)
  • cpu tick array
  • current frequency
  • which physical processor (core) it's on
  • which package/socket it's on
  • whether it's currently in low-power mode
  • it can inherit from the parent CentralProcessor object so you can ask it for higher level info (Vendor, Model, advertised frequency) or if it does not inherit, at least have a getter to the "parent" processor.

@YoshiEnVerde
Copy link
Collaborator

Be aware that I'm not actually saying this should be the exact way we should do this. I tend to design for higher level solutions, so I use more abstraction than the project might need.

I'm more interested in setting things up in a way we can later do this kind of thing.

If we're going to decouple fetching from API and Caching, there's nothing keeping us from doing a low-level API and a high-level API at the same time:

  • Fetching Layer:
    • Works as bare metal as possible
    • Drivers fetch blocks of values each refresh and parse them as needed per request
    • Errors are thrown as exceptions.
  • Caching Layer:
    • Optional
    • Can be used to avoid using the drivers whenever possible
  • Low-Level API:
    • Functionalities are implemented with values as primitive as possible
    • Avoid as much caching and object creation as possible
    • Errors return standardized default values
  • High-Level API:
    • Complex set of interfaces to handle standardization/specialization
    • Use caching and objects as needed
    • Errors are handled through pseudo-optional Result objects

@YoshiEnVerde
Copy link
Collaborator

* String and Boolean returns by default

An easy case. Or not really. What if the boolean result is "unknown"?

Then, by mathematical definition, it's not a boolean result.
It's a ternary result that can be {true, false, "unknown"}
In which case, you can choose between three base implementations:

  • Boolean {true, false, null}
  • String {"true", "false", "unknown"}
  • Enum {TRUE, FALSE, UNKNOWN}
    Or use a complex response solution (exceptions, response objects, etc) to allow for error results
* Numeric returns if the type of value returned could **never** be anything but numeric (like frecuencies, latencies, sizes, etc)

Do you literally mean the Java 'Number' object? Because we have access to UINT64 data that should be properly returned as a BigDecimal but right now we just strip the sign bit and return a long.

Nah, just used Numeric to avoid having to list every single numeric type we have access to. We can use primitives, boxed, atomics, whatever corresponds to the type needed.

Good point. We probably don't need the whole inheritance structure and can just do a List<LogicalProcessor> then, with each LogicalProcessor object containing [...]
Exactly.

The main detail here is that, with the API and the Drivers decoupled, there is no reason for the API to mirror anything in the real machine. We only need to know how to map between them.

The API should be designed to be usable by the users, to be functional to whatever we decide to do with this version of the library.

For example, in that Processor detail you give:
Why would you want to get higher level info from the LogicalProcessor interface?
It's already there in the PhysicalProcessor interface, or the main Processor interface.
Duplication of values is one of those things you need to avoid unless it's necessary to do it.

At most, you just add an ID for the Processor it belongs to, and the user can use that to filter the PhysicalProcessor list.
Most probably, you just add a reference to the parent object.

Also, don't ofrget that, if the only thing we take from the PhysicalProcessor interface is the List it contains, we can just as well have the full list of all logical processors in the main parent interface, and add that PhysProc ID to the LogProc in some ParentProcessorId field, or something

@cilki
Copy link
Collaborator

cilki commented Jan 3, 2019

So the high-level API uses the low-level and is also the only part visible to the user? Or does the user choose which to use according to caching requirements?

@dbwiddis
Copy link
Member Author

dbwiddis commented Jan 3, 2019

Why would you want to get higher level info from the LogicalProcessor interface?

If I wanted to calculate over- or under-clocking ratio. Say my CPU (as reported by the CentralProcessor object) is rated at 3.4 GHz and I'm overclocking to 3.5 GHz. The Vendor frequency will still tell me 3.4 GHz but the actual processor speed (from LogicalProcessor object) will report 3.5 Ghz. Or maybe my processors "sleep" in idle to save power and it will show 1.7 GHz. I'd like to see that "50%" ratio.

Duplication of values is one of those things you need to avoid

I'm not duplicating the value. I'm either using inheritance to access the parent CPU's getVendorFrequency() method or I've got a class variable pointing to the CPU object so I can do logProc.getTheCpuIAmOn().getVendorFrequency(). The value lives in one place; it's easy access to it from an individual object that's relevant. Or I can insist that I always have access to the parent (when I get my list of logical processor objects) and do the calculations "outside" of the logical processor object.

@YoshiEnVerde
Copy link
Collaborator

What I meant with my question was not what you'd do with the info itself, but why you'd want to access it through the LogicalProcessor object.

If there's a chance different LogProcs within the same PhysProc might have different values for Frecuency, then we could consider the field intrinsic to the LogProc, add it in, and in the cases all LogProcs share the same Frecueency as their parent PhysProc, we'd load the value from there.

Another detail on this issue would be that I don't think we should do inheritance between the PhysicalProcessor and the LogicalProcessor classes. They're not Parent/Child, they're both Siblings under the main Processor class (and, even then, they might all be siblings under a specific interface, but not hierarchically linked)

@dbwiddis
Copy link
Member Author

dbwiddis commented Jan 4, 2019

why you'd want to access it through the LogicalProcessor object.

So that I could have a getCurrentFrequencyRatio() method on the LogicalProcessor object itself or with a method on another class that takes only the LogicalProcessor as an argument. Fundamentally, a Logical Processor does have a "vendor frequency" value which is common across all logical processors.

I don't think we should do inheritance between the PhysicalProcessor and the LogicalProcessor classes

Agreed. Actually, I think an inner class might be the correct answer here. A CentralProcessor.LogicalProcessor class would have its own currentFrequency value and could easily access its parent class's getVendorFrequency() method (or even the private field using CentralProcessor.this.vendorFreq

@YoshiEnVerde
Copy link
Collaborator

Ahh, the issue is that you're putting the results and the values in the same-ish bag.

Continuing with the example we've been working over, we don't need to have the Frequency as part of the LogicalProcessor. What we need is for the LogProc to internally know which PhysicalProcessor is their parent, so that when asked what their CurrentFrequencyRatio is, they can fetch the base frequency from their parent processor.

How I've been thinking of this is by using those multiple layers we've been talking about to separate logic from entities, and entities from values.

Basically, we have the following layers:

  1. Values: These are the primitives of OSHI, the values recovered from the system and formatted by our code. Anything at this layer should be final values, or objects external to OSHI. E.g.: the value of the S/N of the MOBO, the frequency ratio of a LogProc, etc
  2. Entities: These are the public facing objects that OSHI delivers with the API. Immutable POJOs that contain the latest values fetched from the system. A call to any of their methods will either return a Value, or another Entity
  3. Public Interface: These are the public facing interfaces that OSHI uses to present the Entities. The general idea is that every entity should implement at least one of these, and the user should not directly reference entities, but the interfaces they implement.
  4. Internal Logic: The set of internal layers that will handle getting the raw Values that populate the Entities requested through the Interface. The meat of the library, where we have the logic that fetches the data from the system, generates and populates the objects given to the user, keeps the data fresh, configures and initializes processes, handles errors, etc. As a general idea, it would involve: Drivers + Caching + Fetchers + Config + Testing

@YoshiEnVerde
Copy link
Collaborator

As an example, using the current OSHI3 elements:

We have a class Networks with a single method NetworkIF[] getNetworks().
The NetworkIF class contains a NetworkInterfaceclass, and it uses that to fetch some of the values it delivers as they are requested, while some of them are initialized at creation (from the same NetworkInterface class)

Now, in OSHI5, we would have a Networks interface in [3], that would work as a collection of NetworkInterface interface implementations, also from [3].
Both of those would have some refresh/update method that would allow for the collection to reload their components, or the interface to reload its internals.

Meanwhile, the NetworkInterface interface would serve the entitites NetworkInterfaceDetailsand NetworkInterfaceUsage from [2]. These would contain all the static values from the NetIF (Details) and the incoming/outgoing values (Usage).
These two entities would be immutable POJOs that just serve the values at the time of request.

So, checking the usage over time of a single NetIF, for example, would mean something like:

Networks networks = <<InsertCorrespondingClassHere>>.getNetworks();

NetworkInterface netif = null;
for(NetworkInterface netinterface : networks.getNetworkInterfaces()) {
   NetworkInterfaceDetails details = netinterface.GetDetails();
   if(<<select the correct netif to use>>)
      netif = netinterface;
}

while(<<check if we're still doing calculations>>) {
   netif.RefreshData();
   NetworkInterfaceUsage usage = netif.GetUsage();
   <<get values from netif and use them>>
   <<delay until next metric is needed>>
}

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants